diff --git a/src/config/bibtexentries.cpp b/src/config/bibtexentries.cpp index 42672840..1eded436 100644 --- a/src/config/bibtexentries.cpp +++ b/src/config/bibtexentries.cpp @@ -1,179 +1,179 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 "bibtexentries.h" #include #ifdef HAVE_KF5 #include #else // HAVE_KF5 #include #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 #include "preferences.h" #include "logging_config.h" bool operator==(const EntryDescription &a, const EntryDescription &b) { return a.upperCamelCase == b.upperCamelCase; } uint qHash(const EntryDescription &a) { return qHash(a.upperCamelCase); } class BibTeXEntries::BibTeXEntriesPrivate { public: static const QVector entryDescriptionsBibTeX; #ifdef HAVE_KF5 static const QVector entryDescriptionsBibLaTeX; #endif // HAVE_KF5 }; BibTeXEntries::BibTeXEntries(const QVector &other) : QVector(other), d(new BibTeXEntriesPrivate()) { /// nothing } BibTeXEntries::~BibTeXEntries() { delete d; } const BibTeXEntries &BibTeXEntries::instance() { static const QVector entryDescriptionsBibTeX { EntryDescription {QStringLiteral("Article"), QString(), i18n("Journal Article"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("journal"), QStringLiteral("year")}, {QStringLiteral("volume"), QStringLiteral("number"), QStringLiteral("pages"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("InProceedings"), i18n("Conference"), QStringLiteral("Publication in Conference Proceedings"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year")}, {QStringLiteral("editor"), QStringLiteral("volume^number"), QStringLiteral("series"), QStringLiteral("pages"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("Proceedings"), QString(), i18n("Conference or Workshop Proceedings"), {QStringLiteral("title"), QStringLiteral("year")}, {QStringLiteral("editor"), QStringLiteral("volume^number"), QStringLiteral("series"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("TechReport"), QString(), i18n("Technical Report"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("institution"), QStringLiteral("year")}, {QStringLiteral("type"), QStringLiteral("number"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("Misc"), QString(), i18n("Miscellaneous"), {}, {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("howpublished"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("Book"), QString(), i18n("Book"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("publisher"), QStringLiteral("year")}, {QStringLiteral("volume^number"), QStringLiteral("series"), QStringLiteral("address"), QStringLiteral("edition"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("InBook"), QString(), i18n("Part of a Book"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("chapter|pages"), QStringLiteral("publisher"), QStringLiteral("year")}, {QStringLiteral("volume^number"), QStringLiteral("series"), QStringLiteral("type"), QStringLiteral("address"), QStringLiteral("edition"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("InCollection"), QString(), i18n("Part of a Book with own Title"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("publisher"), QStringLiteral("year")}, {QStringLiteral("editor"), QStringLiteral("volume^number"), QStringLiteral("series"), QStringLiteral("type"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("address"), QStringLiteral("edition"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("PhDThesis"), QString(), i18n("PhD Thesis"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("school"), QStringLiteral("year")}, {QStringLiteral("type"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("MastersThesis"), QString(), i18n("Master's Thesis"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("school"), QStringLiteral("year")}, {QStringLiteral("type"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("Unpublished"), QString(), i18n("Unpublished Material"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("note")}, {QStringLiteral("month"), QStringLiteral("year")}}, EntryDescription {QStringLiteral("Manual"), QString(), i18n("Manual"), {QStringLiteral("title")}, {QStringLiteral("author"), QStringLiteral("organization"), QStringLiteral("address"), QStringLiteral("edition"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("note")}}, EntryDescription {QStringLiteral("Booklet"), QString(), i18n("Booklet"), {QStringLiteral("title")}, {QStringLiteral("author"), QStringLiteral("howpublished"), QStringLiteral("address"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("note")}} }; #ifdef HAVE_KF5 static const QVector entryDescriptionsBibLaTeX { EntryDescription {QStringLiteral("Article"), QString(), i18n("Journal Article"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("journaltitle"), QStringLiteral("year^date")}, {QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("journalsubtitle"), QStringLiteral("issuetitle"), QStringLiteral("issuesubtitle"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("series"), QStringLiteral("volume"), QStringLiteral("number"), QStringLiteral("eid"), QStringLiteral("issue"), QStringLiteral("month"), QStringLiteral("pages"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("issn"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Book"), QString(), i18n("Book"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("MVBook"), QString(), i18n("Multi-volume Book"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("InBook"), QString(), i18n("Part of a Book"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("bookauthor"), QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("BookInBook"), QString(), i18n("Former Monograph as Part of a Book"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("bookauthor"), QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("SuppBook"), QString(), i18n("Supplemental Material in a Book"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("bookauthor"), QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Booklet"), QString(), i18n("Booklet"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("howpublished"), QStringLiteral("type"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Collection"), QString(), i18n("Single-volume Collection"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("MVCollection"), QString(), i18n("Multi-volume Collection"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("InCollection"), QString(), i18n("Part of a Book with own Title"), {QStringLiteral("author"), QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("SuppCollection"), QString(), i18n("Supplemental Material in a Collection"), {QStringLiteral("author"), QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Manual"), QString(), i18n("Manual"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("edition"), QStringLiteral("type"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Misc"), QString(), i18n("Miscellaneous"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("howpublished"), QStringLiteral("type"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("location"), QStringLiteral("date"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Online"), QStringLiteral("Electronic"), i18n("Online Resource"), {QStringLiteral("author^editor"), QStringLiteral("title"), QStringLiteral("year^date"), QStringLiteral("url")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("date"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Patent"), QString(), i18n("Patent"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("number"), QStringLiteral("year^date")}, {QStringLiteral("holder"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("type"), QStringLiteral("version"), QStringLiteral("location"), QStringLiteral("note"), QStringLiteral("date"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Periodical"), QString(), i18n("Periodical (Complete Issue)"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("subtitle"), QStringLiteral("issuetitle"), QStringLiteral("issuesubtitle"), QStringLiteral("language"), QStringLiteral("series"), QStringLiteral("volume"), QStringLiteral("number"), QStringLiteral("issue"), QStringLiteral("date"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("note"), QStringLiteral("issn"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("SuppPeriodical"), QString(), i18n("Supplemental Material in a Periodical"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("journaltitle"), QStringLiteral("year^date")}, {QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("editor"), QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("journalsubtitle"), QStringLiteral("issuetitle"), QStringLiteral("issuesubtitle"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("series"), QStringLiteral("volume"), QStringLiteral("number"), QStringLiteral("eid"), QStringLiteral("issue"), QStringLiteral("month"), QStringLiteral("pages"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("issn"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Proceedings"), QString(), i18n("Conference or Workshop Proceedings"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("eventtitle"), QStringLiteral("eventdate"), QStringLiteral("venue"), QStringLiteral("language"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("MVProceedings"), QString(), i18n("Multi-volume Proceedings"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("eventtitle"), QStringLiteral("eventdate"), QStringLiteral("venue"), QStringLiteral("language"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("InProceedings"), QStringLiteral("Conference"), i18n("Publication in Conference Proceedings"), {QStringLiteral(" author"), QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("eventtitle"), QStringLiteral("eventdate"), QStringLiteral("venue"), QStringLiteral("language"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("organization"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Reference"), QString(), i18n("Single-volume Reference (e.g. Encyclopedia)"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("MVReference"), QString(), i18n("Multi-volume Reference (e.g. Encyclopedia)"), {QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("InReference"), QString(), i18n("Part of a Reference"), {QStringLiteral("author"), QStringLiteral("editor"), QStringLiteral("title"), QStringLiteral("booktitle"), QStringLiteral("year^date")}, {QStringLiteral("editora"), QStringLiteral("editorb"), QStringLiteral("editorc"), QStringLiteral("translator"), QStringLiteral("annotator"), QStringLiteral("commentator"), QStringLiteral("introduction"), QStringLiteral("foreword"), QStringLiteral("afterword"), QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("maintitle"), QStringLiteral("mainsubtitle"), QStringLiteral("maintitleaddon"), QStringLiteral("booksubtitle"), QStringLiteral("booktitleaddon"), QStringLiteral("language"), QStringLiteral("origlanguage"), QStringLiteral("volume"), QStringLiteral("part"), QStringLiteral("edition"), QStringLiteral("volumes"), QStringLiteral("series"), QStringLiteral("number"), QStringLiteral("note"), QStringLiteral("publisher"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Report"), QString(), i18n("Report"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("type"), QStringLiteral("institution"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("number"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isrn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Thesis"), QString(), i18n("Thesis"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("type"), QStringLiteral("institution"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("Unpublished"), QString(), i18n("Unpublished Material"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("language"), QStringLiteral("howpublished"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("isbn"), QStringLiteral("date"), QStringLiteral("month"), QStringLiteral("year"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("MastersThesis"), QString(), i18n("Master's Thesis"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("institution"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("type"), QStringLiteral("language"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("PhDThesis"), QString(), i18n("PhD Thesis"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("institution"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("type"), QStringLiteral("language"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isbn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}}, EntryDescription {QStringLiteral("TechReport"), QString(), i18n("Technical Report"), {QStringLiteral("author"), QStringLiteral("title"), QStringLiteral("institution"), QStringLiteral("year^date")}, {QStringLiteral("subtitle"), QStringLiteral("titleaddon"), QStringLiteral("type"), QStringLiteral("language"), QStringLiteral("number"), QStringLiteral("version"), QStringLiteral("note"), QStringLiteral("location"), QStringLiteral("month"), QStringLiteral("isrn"), QStringLiteral("chapter"), QStringLiteral("pages"), QStringLiteral("pagetotal"), QStringLiteral("addendum"), QStringLiteral("pubstate"), QStringLiteral("doi"), QStringLiteral("eprint"), QStringLiteral("eprintclass"), QStringLiteral("eprinttype"), QStringLiteral("url"), QStringLiteral("urldate")}} }; #endif // HAVE_KF5 static const BibTeXEntries singletonBibTeX(entryDescriptionsBibTeX) #ifdef HAVE_KF5 , singletonBibLaTeX(entryDescriptionsBibLaTeX) #endif // HAVE_KF5 ; #ifdef HAVE_KF5 - return Preferences::instance().bibliographySystem() == Preferences::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; + return Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; #else // HAVE_KF5 return singletonBibTeX; #endif // HAVE_KF5 } QString BibTeXEntries::format(const QString &name, KBibTeX::Casing casing) const { QString iName = name.toLower(); switch (casing) { - case KBibTeX::cLowerCase: return iName; - case KBibTeX::cUpperCase: return name.toUpper(); - case KBibTeX::cInitialCapital: + case KBibTeX::Casing::LowerCase: return iName; + case KBibTeX::Casing::UpperCase: return name.toUpper(); + case KBibTeX::Casing::InitialCapital: iName[0] = iName[0].toUpper(); return iName; - case KBibTeX::cLowerCamelCase: { + case KBibTeX::Casing::LowerCamelCase: { for (const auto &ed : const_cast(*this)) { /// configuration file uses camel-case QString itName = ed.upperCamelCase.toLower(); if (itName == iName) { iName = ed.upperCamelCase; break; } } /// make an educated guess how camel-case would look like iName[0] = iName[0].toLower(); return iName; } - case KBibTeX::cUpperCamelCase: { + case KBibTeX::Casing::UpperCamelCase: { for (const auto &ed : const_cast(*this)) { /// configuration file uses camel-case QString itName = ed.upperCamelCase.toLower(); if (itName == iName) { iName = ed.upperCamelCase; break; } } /// make an educated guess how camel-case would look like iName[0] = iName[0].toUpper(); return iName; } } return name; } QString BibTeXEntries::label(const QString &name) const { const QString iName = name.toLower(); for (const auto &ed : const_cast(*this)) { /// Configuration file uses camel-case, convert this to lower case for faster comparison QString itName = ed.upperCamelCase.toLower(); if (itName == iName || (!(itName = ed.upperCamelCaseAlt.toLower()).isEmpty() && itName == iName)) return ed.label; } return QString(); } diff --git a/src/config/bibtexfields.cpp b/src/config/bibtexfields.cpp index c441a01d..36e63de7 100644 --- a/src/config/bibtexfields.cpp +++ b/src/config/bibtexfields.cpp @@ -1,413 +1,413 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 "bibtexfields.h" #include #include #ifdef HAVE_KF5 #include #include #include #else // HAVE_KF5 #define i18n(text) QObject::tr(text) #endif // HAVE KF5 #include "preferences.h" #include "logging_config.h" bool operator==(const FieldDescription &a, const FieldDescription &b) { return a.upperCamelCase == b.upperCamelCase; } uint qHash(const FieldDescription &a) { return qHash(a.upperCamelCase); } class BibTeXFields::BibTeXFieldsPrivate { public: static const QVector fieldDescriptionsBibTeX; static const QVector fieldDescriptionsBibLaTeX; BibTeXFields *p; #ifdef HAVE_KF5 KSharedConfigPtr layoutConfig; #endif // HAVE_KF5 BibTeXFieldsPrivate(const QString &style, BibTeXFields *parent) : p(parent) { #ifdef HAVE_KF5 const QString stylefile = style + QStringLiteral(".kbstyle"); layoutConfig = KSharedConfig::openConfig(stylefile, KConfig::FullConfig, QStandardPaths::AppDataLocation); if (layoutConfig->groupList().isEmpty()) qCWarning(LOG_KBIBTEX_CONFIG) << "The configuration file for layout of type" << style << "could not be located or is empty"; #else // HAVE_KF5 Q_UNUSED(style) #endif // HAVE_KF5 } #ifdef HAVE_KF5 void load() { for (BibTeXFields::Iterator it = p->begin(); it != p->end(); ++it) { auto &fd = *it; const QString groupName = QStringLiteral("Column") + fd.upperCamelCase + fd.upperCamelCaseAlt; KConfigGroup configGroup(layoutConfig, groupName); fd.visible.clear(); if (configGroup.exists()) { const QStringList keyList = configGroup.keyList(); for (const QString &key : keyList) { if (!key.startsWith(QStringLiteral("Visible_"))) continue; ///< a key other than a 'visibility' key const QString treeViewName = key.mid(8); fd.visible.insert(treeViewName, configGroup.readEntry(key, fd.defaultVisible)); } } } } void save() { for (const auto &fd : const_cast(*p)) { const QString groupName = QStringLiteral("Column") + fd.upperCamelCase + fd.upperCamelCaseAlt; KConfigGroup configGroup(layoutConfig, groupName); const QList keys = fd.visible.keys(); for (const QString &treeViewName : keys) { const QString key = QStringLiteral("Visible_") + treeViewName; configGroup.writeEntry(key, fd.visible.value(treeViewName, fd.defaultVisible)); } } layoutConfig->sync(); } void resetToDefaults(const QString &treeViewName) { for (BibTeXFields::Iterator it = p->begin(); it != p->end(); ++it) { const QString groupName = QStringLiteral("Column") + it->upperCamelCase + it->upperCamelCaseAlt; KConfigGroup configGroup(layoutConfig, groupName); configGroup.deleteEntry("Visible_" + treeViewName); } layoutConfig->sync(); load(); } #endif // HAVE_KF5 }; BibTeXFields::BibTeXFields(const QString &style, const QVector &other) : QVector(other), d(new BibTeXFieldsPrivate(style, this)) { #ifdef HAVE_KF5 d->load(); #endif // HAVE_KF5 } BibTeXFields::~BibTeXFields() { delete d; } /// This function cannot return a 'const' BibTeXFields object /// similarly like BibTeXEntries::instance as the FieldDescription's /// QMap visible will be modified. BibTeXFields &BibTeXFields::instance() { static const QVector fieldDescriptionsBibTeX { - FieldDescription {QStringLiteral("^type"), QString(), {}, i18n("Element Type"), KBibTeX::tfSource, KBibTeX::tfSource, 5, {}, true, true}, - FieldDescription {QStringLiteral("^id"), QString(), {}, i18n("Identifier"), KBibTeX::tfSource, KBibTeX::tfSource, 6, {}, true, true}, - FieldDescription {QStringLiteral("Title"), QString(), {}, i18n("Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("Title"), QStringLiteral("BookTitle"), {}, i18n("Title or Book Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, true, false}, - FieldDescription {QStringLiteral("Author"), QStringLiteral("Editor"), {}, i18n("Author or Editor"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 7, {}, true, false}, - FieldDescription {QStringLiteral("Author"), QString(), {}, i18n("Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Editor"), QString(), {}, i18n("Editor"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Month"), QString(), {}, i18n("Month"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("Year"), QString(), {}, i18n("Year"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, true, false}, - FieldDescription {QStringLiteral("Journal"), QString(), {}, i18n("Journal"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("Volume"), QString(), {}, i18n("Volume"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Number"), QString(), {}, i18n("Number"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("ISSN"), QString(), {}, i18n("ISSN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("ISBN"), QString(), {}, i18n("ISBN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("ISBN"), QStringLiteral("ISSN"), {}, i18n("ISBN or ISSN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("HowPublished"), QString(), {}, i18n("How Published"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Note"), QString(), {}, i18n("Note"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Abstract"), QString(), {}, i18n("Abstract"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 7, {}, false, true}, - FieldDescription {QStringLiteral("Pages"), QString(), {}, i18n("Pages"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, true, false}, - FieldDescription {QStringLiteral("Publisher"), QString(), {}, i18n("Publisher"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Institution"), QString(), {}, i18n("Institution"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("BookTitle"), QString(), {}, i18n("Book Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("Series"), QString(), {}, i18n("Series"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 12, {}, false, false}, - FieldDescription {QStringLiteral("Edition"), QString(), {}, i18n("Edition"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Chapter"), QString(), {}, i18n("Chapter"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Organization"), QString(), {}, i18n("Organization"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("School"), QString(), {}, i18n("School"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Keywords"), QString(), {}, i18n("Keywords"), KBibTeX::tfKeyword, KBibTeX::tfKeyword | KBibTeX::tfSource, 3, {}, false, true}, - FieldDescription {QStringLiteral("CrossRef"), QString(), {}, i18n("Cross Reference"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, true}, - FieldDescription {QStringLiteral("DOI"), QString(), {}, i18n("DOI"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 1, {}, false, true}, - FieldDescription {QStringLiteral("URL"), QString(), {}, i18n("URL"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, true}, - FieldDescription {QStringLiteral("Location"), QString(), {}, i18n("Location"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("Address"), QString(), {}, i18n("Address"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("Type"), QString(), {}, i18n("Type"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Key"), QString(), {}, i18n("Key"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("X-Color"), QString(), {}, i18n("Color"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("X-Stars"), QString(), {}, i18n("Stars"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim | KBibTeX::tfSource, 4, {}, false, true}, + FieldDescription {QStringLiteral("^type"), QString(), {}, i18n("Element Type"), KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, 5, {}, true, true}, + FieldDescription {QStringLiteral("^id"), QString(), {}, i18n("Identifier"), KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, 6, {}, true, true}, + FieldDescription {QStringLiteral("Title"), QString(), {}, i18n("Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("Title"), QStringLiteral("BookTitle"), {}, i18n("Title or Book Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, true, false}, + FieldDescription {QStringLiteral("Author"), QStringLiteral("Editor"), {}, i18n("Author or Editor"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 7, {}, true, false}, + FieldDescription {QStringLiteral("Author"), QString(), {}, i18n("Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Editor"), QString(), {}, i18n("Editor"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Month"), QString(), {}, i18n("Month"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("Year"), QString(), {}, i18n("Year"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, true, false}, + FieldDescription {QStringLiteral("Journal"), QString(), {}, i18n("Journal"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("Volume"), QString(), {}, i18n("Volume"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Number"), QString(), {}, i18n("Number"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("ISSN"), QString(), {}, i18n("ISSN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("ISBN"), QString(), {}, i18n("ISBN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("ISBN"), QStringLiteral("ISSN"), {}, i18n("ISBN or ISSN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("HowPublished"), QString(), {}, i18n("How Published"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Note"), QString(), {}, i18n("Note"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Abstract"), QString(), {}, i18n("Abstract"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 7, {}, false, true}, + FieldDescription {QStringLiteral("Pages"), QString(), {}, i18n("Pages"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, true, false}, + FieldDescription {QStringLiteral("Publisher"), QString(), {}, i18n("Publisher"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Institution"), QString(), {}, i18n("Institution"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("BookTitle"), QString(), {}, i18n("Book Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("Series"), QString(), {}, i18n("Series"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 12, {}, false, false}, + FieldDescription {QStringLiteral("Edition"), QString(), {}, i18n("Edition"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Chapter"), QString(), {}, i18n("Chapter"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Organization"), QString(), {}, i18n("Organization"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("School"), QString(), {}, i18n("School"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Keywords"), QString(), {}, i18n("Keywords"), KBibTeX::TypeFlag::Keyword, KBibTeX::TypeFlag::Keyword | KBibTeX::TypeFlag::Source, 3, {}, false, true}, + FieldDescription {QStringLiteral("CrossRef"), QString(), {}, i18n("Cross Reference"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, true}, + FieldDescription {QStringLiteral("DOI"), QString(), {}, i18n("DOI"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 1, {}, false, true}, + FieldDescription {QStringLiteral("URL"), QString(), {}, i18n("URL"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, true}, + FieldDescription {QStringLiteral("Location"), QString(), {}, i18n("Location"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("Address"), QString(), {}, i18n("Address"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("Type"), QString(), {}, i18n("Type"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Key"), QString(), {}, i18n("Key"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("X-Color"), QString(), {}, i18n("Color"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("X-Stars"), QString(), {}, i18n("Stars"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim | KBibTeX::TypeFlag::Source, 4, {}, false, true}, }; static const QVector fieldDescriptionsBibLaTeX { - FieldDescription {QStringLiteral("^type"), QString(), {}, i18n("Element Type"), KBibTeX::tfSource, KBibTeX::tfSource, 5, {}, true, true}, - FieldDescription {QStringLiteral("^id"), QString(), {}, i18n("Identifier"), KBibTeX::tfSource, KBibTeX::tfSource, 6, {}, true, true}, - FieldDescription {QStringLiteral("Title"), QString(), {}, i18n("Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("Title"), QStringLiteral("BookTitle"), {}, i18n("Title or Book Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, true, false}, - FieldDescription {QStringLiteral("SubTitle"), QString(), {}, i18n("Subtitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("TitleAddon"), QString(), {}, i18n("Title Addon"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("ShortTitle"), QString(), {}, i18n("Shortitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("OrigTitle"), QString(), {}, i18n("Original Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ReprintTitle"), QString(), {}, i18n("Reprint Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("MainTitle"), QString(), {}, i18n("Main Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("MainSubTitle"), QString(), {}, i18n("Main Subtitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("MainTitleAddon"), QString(), {}, i18n("Maintitle Addon"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("Author"), QStringLiteral("Editor"), {}, i18n("Author or Editor"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 7, {}, true, false}, - FieldDescription {QStringLiteral("Author"), QString(), {}, i18n("Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("ShortAuthor"), QString(), {}, i18n("Short Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("NameAddon"), QString(), {}, i18n("Name Addon"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("AuthorType"), QString(), {}, i18n("Author Type"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("BookAuthor"), QString(), {}, i18n("Book Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Editor"), QString(), {}, i18n("Editor"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("ShortEditor"), QString(), {}, i18n("Short Editor"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorType"), QString(), {}, i18n("Editor Type"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorA"), QString(), {}, i18n("Editor A"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorAType"), QString(), {}, i18n("Editor A Type"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorB"), QString(), {}, i18n("Editor B"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorBType"), QString(), {}, i18n("Editor B Type"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorC"), QString(), {}, i18n("Editor C"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("EditorCType"), QString(), {}, i18n("Editor C Type"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Translator"), QString(), {}, i18n("Translator"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Afterword"), QString(), {}, i18n("Afterword Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Introduction"), QString(), {}, i18n("Introduction Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Foreword"), QString(), {}, i18n("Foreword Author"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Annotator"), QString(), {}, i18n("Annotator"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Commentator"), QString(), {}, i18n("Commentator"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Holder"), QString(), {}, i18n("Patent Holder"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("Month"), QString(), {}, i18n("Month"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("Year"), QString(), {}, i18n("Year"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Date"), QString(), {}, i18n("Date"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Year"), QStringLiteral("Date"), {}, i18n("Date or Year"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, true, false}, - FieldDescription {QStringLiteral("EventDate"), QString(), {}, i18n("Event Date"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("OrigDate"), QString(), {}, i18n("Original Date"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("JournalTitle"), QString(), {QStringLiteral("Journal")}, i18n("Journal Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("JournalSubTitle"), QString(), {}, i18n("Journal Subtitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("ShortJournal"), QString(), {}, i18n("Journal Shortitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("Volume"), QString(), {}, i18n("Volume"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Volumes"), QString(), {}, i18n("Number of Volumes"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Number"), QString(), {}, i18n("Number"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Version"), QString(), {}, i18n("Version"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Part"), QString(), {}, i18n("Part"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Issue"), QString(), {}, i18n("Issue"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("IASN"), QString(), {}, i18n("IASN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISMN"), QString(), {}, i18n("ISMN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISRN"), QString(), {}, i18n("ISRN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISSN"), QString(), {}, i18n("ISSN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISBN"), QString(), {}, i18n("ISBN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISBN"), QStringLiteral("ISSN"), {}, i18n("ISBN or ISSN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("ISWC"), QString(), {}, i18n("ISWC"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("HowPublished"), QString(), {}, i18n("How Published"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Note"), QString(), {}, i18n("Note"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Addendum"), QString(), {}, i18n("Addendum"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Annotation"), QString(), {QStringLiteral("Annote")}, i18n("Annotation"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("Abstract"), QString(), {}, i18n("Abstract"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 7, {}, false, true}, - FieldDescription {QStringLiteral("Pages"), QString(), {}, i18n("Pages"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, true, false}, - FieldDescription {QStringLiteral("PageTotal"), QString(), {}, i18n("Total Pages"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Pagination"), QString(), {}, i18n("Pagination"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("BookPagination"), QString(), {}, i18n("Book Pagination"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Publisher"), QString(), {}, i18n("Publisher"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("OrigPublisher"), QString(), {}, i18n("Original Publisher"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Institution"), QString(), {QStringLiteral("School")}, i18n("Institution"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 5, {}, false, false}, - FieldDescription {QStringLiteral("BookTitle"), QString(), {}, i18n("Book Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("BookSubTitle"), QString(), {}, i18n("Book Subtitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("IssueTitle"), QString(), {}, i18n("Issue Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("IssueSubTitle"), QString(), {}, i18n("Issue Subtitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("BookTitleAddon"), QString(), {}, i18n("Booktitle Addon"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 14, {}, false, false}, - FieldDescription {QStringLiteral("Series"), QString(), {}, i18n("Series"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 12, {}, false, false}, - FieldDescription {QStringLiteral("ShortSeries"), QString(), {}, i18n("Series Shortitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 4, {}, false, false}, - FieldDescription {QStringLiteral("Edition"), QString(), {}, i18n("Edition"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Chapter"), QString(), {}, i18n("Chapter"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 1, {}, false, false}, - FieldDescription {QStringLiteral("Organization"), QString(), {}, i18n("Organization"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("EventTitle"), QString(), {}, i18n("Event Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Venue"), QString(), {}, i18n("Venue"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("IndexTitle"), QString(), {}, i18n("Index Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("Keywords"), QString(), {}, i18n("Keywords"), KBibTeX::tfKeyword, KBibTeX::tfKeyword | KBibTeX::tfSource, 3, {}, false, true}, - FieldDescription {QStringLiteral("CrossRef"), QString(), {}, i18n("Cross Reference"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, true}, - FieldDescription {QStringLiteral("XRef"), QString(), {}, i18n("XRef"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, true}, - FieldDescription {QStringLiteral("DOI"), QString(), {}, i18n("DOI"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 1, {}, false, false}, - FieldDescription {QStringLiteral("EPrint"), QString(), {}, i18n("E-Print"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 1, {}, false, false}, - FieldDescription {QStringLiteral("EPrintClass"), QString(), {QStringLiteral("PrimaryClass")}, i18n("E-Print Class"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("EPrintType"), QString(), {QStringLiteral("ArchivePrefix")}, i18n("E-Print Type"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("URL"), QString(), {}, i18n("URL"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, false}, - FieldDescription {QStringLiteral("URLDate"), QString(), {}, i18n("URL Date"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("File"), QString(), {QStringLiteral("PDF")}, i18n("Local File URL"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, 3, {}, false, true}, - FieldDescription {QStringLiteral("Location"), QString(), {QStringLiteral("Address")}, i18n("Location"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, - FieldDescription {QStringLiteral("OrigLocation"), QString(), {}, i18n("Original Location"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Type"), QString(), {}, i18n("Type"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("EID"), QString(), {}, i18n("EID"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Label"), QString(), {}, i18n("Label"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("ShortHand"), QString(), {}, i18n("Shorthand"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("ShortHandIntro"), QString(), {}, i18n("Shorthand Intro"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("PubState"), QString(), {}, i18n("Publication State"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Language"), QString(), {}, i18n("Language"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("OrigLanguage"), QString(), {}, i18n("Original Language"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Library"), QString(), {}, i18n("Library"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("X-Color"), QString(), {}, i18n("Color"), KBibTeX::tfVerbatim, KBibTeX::tfVerbatim | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("Gender"), QString(), {}, i18n("Gender"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("Hyphenation"), QString(), {}, i18n("Hyphenation"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("IndexSortTitle"), QString(), {}, i18n("Index Sorttitle"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("Options"), QString(), {}, i18n("Entry Options"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("Presort"), QString(), {}, i18n("Presort"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("SortKey"), QString(), {QStringLiteral("Key")}, i18n("Sort Key"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("SortName"), QString(), {}, i18n("Sort Names"), KBibTeX::tfPerson, KBibTeX::tfPerson | KBibTeX::tfReference, 7, {}, false, false}, - FieldDescription {QStringLiteral("SortShortHand"), QString(), {}, i18n("Sort Shorthand"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("SortTitle"), QString(), {}, i18n("Sort Title"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("SortYear"), QString(), {}, i18n("Sort Year"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, true}, - FieldDescription {QStringLiteral("ISAN"), QString(), {}, i18n("ISAN"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 2, {}, false, false}, - FieldDescription {QStringLiteral("Location"), QString(), {}, i18n("Location"), KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfReference | KBibTeX::tfSource, 3, {}, false, false}, + FieldDescription {QStringLiteral("^type"), QString(), {}, i18n("Element Type"), KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, 5, {}, true, true}, + FieldDescription {QStringLiteral("^id"), QString(), {}, i18n("Identifier"), KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, 6, {}, true, true}, + FieldDescription {QStringLiteral("Title"), QString(), {}, i18n("Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("Title"), QStringLiteral("BookTitle"), {}, i18n("Title or Book Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, true, false}, + FieldDescription {QStringLiteral("SubTitle"), QString(), {}, i18n("Subtitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("TitleAddon"), QString(), {}, i18n("Title Addon"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("ShortTitle"), QString(), {}, i18n("Shortitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("OrigTitle"), QString(), {}, i18n("Original Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ReprintTitle"), QString(), {}, i18n("Reprint Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("MainTitle"), QString(), {}, i18n("Main Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("MainSubTitle"), QString(), {}, i18n("Main Subtitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("MainTitleAddon"), QString(), {}, i18n("Maintitle Addon"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("Author"), QStringLiteral("Editor"), {}, i18n("Author or Editor"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 7, {}, true, false}, + FieldDescription {QStringLiteral("Author"), QString(), {}, i18n("Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("ShortAuthor"), QString(), {}, i18n("Short Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("NameAddon"), QString(), {}, i18n("Name Addon"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("AuthorType"), QString(), {}, i18n("Author Type"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("BookAuthor"), QString(), {}, i18n("Book Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Editor"), QString(), {}, i18n("Editor"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("ShortEditor"), QString(), {}, i18n("Short Editor"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorType"), QString(), {}, i18n("Editor Type"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorA"), QString(), {}, i18n("Editor A"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorAType"), QString(), {}, i18n("Editor A Type"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorB"), QString(), {}, i18n("Editor B"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorBType"), QString(), {}, i18n("Editor B Type"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorC"), QString(), {}, i18n("Editor C"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("EditorCType"), QString(), {}, i18n("Editor C Type"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Translator"), QString(), {}, i18n("Translator"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Afterword"), QString(), {}, i18n("Afterword Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Introduction"), QString(), {}, i18n("Introduction Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Foreword"), QString(), {}, i18n("Foreword Author"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Annotator"), QString(), {}, i18n("Annotator"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Commentator"), QString(), {}, i18n("Commentator"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Holder"), QString(), {}, i18n("Patent Holder"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("Month"), QString(), {}, i18n("Month"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("Year"), QString(), {}, i18n("Year"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Date"), QString(), {}, i18n("Date"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Year"), QStringLiteral("Date"), {}, i18n("Date or Year"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, true, false}, + FieldDescription {QStringLiteral("EventDate"), QString(), {}, i18n("Event Date"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("OrigDate"), QString(), {}, i18n("Original Date"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("JournalTitle"), QString(), {QStringLiteral("Journal")}, i18n("Journal Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("JournalSubTitle"), QString(), {}, i18n("Journal Subtitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("ShortJournal"), QString(), {}, i18n("Journal Shortitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("Volume"), QString(), {}, i18n("Volume"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Volumes"), QString(), {}, i18n("Number of Volumes"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Number"), QString(), {}, i18n("Number"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Version"), QString(), {}, i18n("Version"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Part"), QString(), {}, i18n("Part"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Issue"), QString(), {}, i18n("Issue"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("IASN"), QString(), {}, i18n("IASN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISMN"), QString(), {}, i18n("ISMN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISRN"), QString(), {}, i18n("ISRN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISSN"), QString(), {}, i18n("ISSN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISBN"), QString(), {}, i18n("ISBN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISBN"), QStringLiteral("ISSN"), {}, i18n("ISBN or ISSN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("ISWC"), QString(), {}, i18n("ISWC"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("HowPublished"), QString(), {}, i18n("How Published"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Note"), QString(), {}, i18n("Note"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Addendum"), QString(), {}, i18n("Addendum"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Annotation"), QString(), {QStringLiteral("Annote")}, i18n("Annotation"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("Abstract"), QString(), {}, i18n("Abstract"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 7, {}, false, true}, + FieldDescription {QStringLiteral("Pages"), QString(), {}, i18n("Pages"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, true, false}, + FieldDescription {QStringLiteral("PageTotal"), QString(), {}, i18n("Total Pages"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Pagination"), QString(), {}, i18n("Pagination"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("BookPagination"), QString(), {}, i18n("Book Pagination"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Publisher"), QString(), {}, i18n("Publisher"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("OrigPublisher"), QString(), {}, i18n("Original Publisher"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Institution"), QString(), {QStringLiteral("School")}, i18n("Institution"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 5, {}, false, false}, + FieldDescription {QStringLiteral("BookTitle"), QString(), {}, i18n("Book Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("BookSubTitle"), QString(), {}, i18n("Book Subtitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("IssueTitle"), QString(), {}, i18n("Issue Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("IssueSubTitle"), QString(), {}, i18n("Issue Subtitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("BookTitleAddon"), QString(), {}, i18n("Booktitle Addon"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 14, {}, false, false}, + FieldDescription {QStringLiteral("Series"), QString(), {}, i18n("Series"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 12, {}, false, false}, + FieldDescription {QStringLiteral("ShortSeries"), QString(), {}, i18n("Series Shortitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 4, {}, false, false}, + FieldDescription {QStringLiteral("Edition"), QString(), {}, i18n("Edition"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Chapter"), QString(), {}, i18n("Chapter"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 1, {}, false, false}, + FieldDescription {QStringLiteral("Organization"), QString(), {}, i18n("Organization"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("EventTitle"), QString(), {}, i18n("Event Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Venue"), QString(), {}, i18n("Venue"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("IndexTitle"), QString(), {}, i18n("Index Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("Keywords"), QString(), {}, i18n("Keywords"), KBibTeX::TypeFlag::Keyword, KBibTeX::TypeFlag::Keyword | KBibTeX::TypeFlag::Source, 3, {}, false, true}, + FieldDescription {QStringLiteral("CrossRef"), QString(), {}, i18n("Cross Reference"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, true}, + FieldDescription {QStringLiteral("XRef"), QString(), {}, i18n("XRef"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, true}, + FieldDescription {QStringLiteral("DOI"), QString(), {}, i18n("DOI"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 1, {}, false, false}, + FieldDescription {QStringLiteral("EPrint"), QString(), {}, i18n("E-Print"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 1, {}, false, false}, + FieldDescription {QStringLiteral("EPrintClass"), QString(), {QStringLiteral("PrimaryClass")}, i18n("E-Print Class"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("EPrintType"), QString(), {QStringLiteral("ArchivePrefix")}, i18n("E-Print Type"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("URL"), QString(), {}, i18n("URL"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, false}, + FieldDescription {QStringLiteral("URLDate"), QString(), {}, i18n("URL Date"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("File"), QString(), {QStringLiteral("PDF")}, i18n("Local File URL"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, 3, {}, false, true}, + FieldDescription {QStringLiteral("Location"), QString(), {QStringLiteral("Address")}, i18n("Location"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, + FieldDescription {QStringLiteral("OrigLocation"), QString(), {}, i18n("Original Location"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Type"), QString(), {}, i18n("Type"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("EID"), QString(), {}, i18n("EID"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Label"), QString(), {}, i18n("Label"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("ShortHand"), QString(), {}, i18n("Shorthand"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("ShortHandIntro"), QString(), {}, i18n("Shorthand Intro"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("PubState"), QString(), {}, i18n("Publication State"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Language"), QString(), {}, i18n("Language"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("OrigLanguage"), QString(), {}, i18n("Original Language"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Library"), QString(), {}, i18n("Library"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("X-Color"), QString(), {}, i18n("Color"), KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("Gender"), QString(), {}, i18n("Gender"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("Hyphenation"), QString(), {}, i18n("Hyphenation"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("IndexSortTitle"), QString(), {}, i18n("Index Sorttitle"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("Options"), QString(), {}, i18n("Entry Options"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("Presort"), QString(), {}, i18n("Presort"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("SortKey"), QString(), {QStringLiteral("Key")}, i18n("Sort Key"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("SortName"), QString(), {}, i18n("Sort Names"), KBibTeX::TypeFlag::Person, KBibTeX::TypeFlag::Person | KBibTeX::TypeFlag::Reference, 7, {}, false, false}, + FieldDescription {QStringLiteral("SortShortHand"), QString(), {}, i18n("Sort Shorthand"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("SortTitle"), QString(), {}, i18n("Sort Title"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("SortYear"), QString(), {}, i18n("Sort Year"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, true}, + FieldDescription {QStringLiteral("ISAN"), QString(), {}, i18n("ISAN"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 2, {}, false, false}, + FieldDescription {QStringLiteral("Location"), QString(), {}, i18n("Location"), KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Reference | KBibTeX::TypeFlag::Source, 3, {}, false, false}, }; static BibTeXFields singletonBibTeX(QStringLiteral("bibtex"), fieldDescriptionsBibTeX), singletonBibLaTeX(QStringLiteral("biblatex"), fieldDescriptionsBibLaTeX); - return Preferences::instance().bibliographySystem() == Preferences::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; + return Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; } #ifdef HAVE_KF5 void BibTeXFields::save() { d->save(); } void BibTeXFields::resetToDefaults(const QString &treeViewName) { d->resetToDefaults(treeViewName); } #endif // HAVE_KF5 QString BibTeXFields::format(const QString &name, KBibTeX::Casing casing) const { QString iName = name.toLower(); switch (casing) { - case KBibTeX::cLowerCase: return iName; - case KBibTeX::cUpperCase: return name.toUpper(); - case KBibTeX::cInitialCapital: + case KBibTeX::Casing::LowerCase: return iName; + case KBibTeX::Casing::UpperCase: return name.toUpper(); + case KBibTeX::Casing::InitialCapital: iName[0] = iName[0].toUpper(); return iName; - case KBibTeX::cLowerCamelCase: { + case KBibTeX::Casing::LowerCamelCase: { for (const auto &fd : const_cast(*this)) { /// configuration file uses camel-case QString itName = fd.upperCamelCase.toLower(); if (itName == iName && fd.upperCamelCaseAlt.isEmpty()) { iName = fd.upperCamelCase; break; } } /// make an educated guess how camel-case would look like iName[0] = iName[0].toLower(); return iName; } - case KBibTeX::cUpperCamelCase: { + case KBibTeX::Casing::UpperCamelCase: { for (const auto &fd : const_cast(*this)) { /// configuration file uses camel-case QString itName = fd.upperCamelCase.toLower(); if (itName == iName && fd.upperCamelCaseAlt.isEmpty()) { iName = fd.upperCamelCase; break; } } /// make an educated guess how camel-case would look like iName[0] = iName[0].toUpper(); return iName; } } return name; } const FieldDescription BibTeXFields::find(const QString &name) const { const QString iName = name.toLower(); for (const auto &fd : const_cast(*this)) { if (fd.upperCamelCase.toLower() == iName && fd.upperCamelCaseAlt.isEmpty()) return fd; } qCWarning(LOG_KBIBTEX_CONFIG) << "No field description for " << name << "(" << iName << ")"; - return FieldDescription {QString(), QString(), {}, QString(), KBibTeX::tfSource, KBibTeX::tfSource, 0, {}, false, false}; + return FieldDescription {QString(), QString(), {}, QString(), KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, 0, {}, false, false}; } KBibTeX::TypeFlag BibTeXFields::typeFlagFromString(const QString &typeFlagString) { - KBibTeX::TypeFlag result = KBibTeX::tfInvalid; + KBibTeX::TypeFlag result = KBibTeX::TypeFlag::Invalid; if (typeFlagString == QStringLiteral("Text")) - result = KBibTeX::tfPlainText; + result = KBibTeX::TypeFlag::PlainText; else if (typeFlagString == QStringLiteral("Source")) - result = KBibTeX::tfSource; + result = KBibTeX::TypeFlag::Source; else if (typeFlagString == QStringLiteral("Person")) - result = KBibTeX::tfPerson; + result = KBibTeX::TypeFlag::Person; else if (typeFlagString == QStringLiteral("Keyword")) - result = KBibTeX::tfKeyword; + result = KBibTeX::TypeFlag::Keyword; else if (typeFlagString == QStringLiteral("Reference")) - result = KBibTeX::tfReference; + result = KBibTeX::TypeFlag::Reference; else if (typeFlagString == QStringLiteral("Verbatim")) - result = KBibTeX::tfVerbatim; + result = KBibTeX::TypeFlag::Verbatim; else qCWarning(LOG_KBIBTEX_CONFIG) << "Could not interpret string" << typeFlagString << "into a KBibTeX::TypeFlag value"; return result; } KBibTeX::TypeFlags BibTeXFields::typeFlagsFromString(const QString &typeFlagsString) { KBibTeX::TypeFlags result; const QStringList list = typeFlagsString.split(';'); for (const QString &s : list) result |= typeFlagFromString(s); return result; } QString BibTeXFields::typeFlagsToString(KBibTeX::TypeFlags typeFlags) { QStringList resultList; - if (typeFlags & KBibTeX::tfPlainText) resultList << QStringLiteral("Text"); - if (typeFlags & KBibTeX::tfSource) resultList << QStringLiteral("Source"); - if (typeFlags & KBibTeX::tfPerson) resultList << QStringLiteral("Person"); - if (typeFlags & KBibTeX::tfKeyword) resultList << QStringLiteral("Keyword"); - if (typeFlags & KBibTeX::tfReference) resultList << QStringLiteral("Reference"); - if (typeFlags & KBibTeX::tfVerbatim) resultList << QStringLiteral("Verbatim"); + if (typeFlags & KBibTeX::TypeFlag::PlainText) resultList << QStringLiteral("Text"); + if (typeFlags & KBibTeX::TypeFlag::Source) resultList << QStringLiteral("Source"); + if (typeFlags & KBibTeX::TypeFlag::Person) resultList << QStringLiteral("Person"); + if (typeFlags & KBibTeX::TypeFlag::Keyword) resultList << QStringLiteral("Keyword"); + if (typeFlags & KBibTeX::TypeFlag::Reference) resultList << QStringLiteral("Reference"); + if (typeFlags & KBibTeX::TypeFlag::Verbatim) resultList << QStringLiteral("Verbatim"); return resultList.join(QChar(';')); } QString BibTeXFields::typeFlagToString(KBibTeX::TypeFlag typeFlag) { - if (typeFlag == KBibTeX::tfPlainText) return QStringLiteral("Text"); - if (typeFlag == KBibTeX::tfSource) return QStringLiteral("Source"); - if (typeFlag == KBibTeX::tfPerson) return QStringLiteral("Person"); - if (typeFlag == KBibTeX::tfKeyword) return QStringLiteral("Keyword"); - if (typeFlag == KBibTeX::tfReference) return QStringLiteral("Reference"); - if (typeFlag == KBibTeX::tfVerbatim) return QStringLiteral("Verbatim"); + if (typeFlag == KBibTeX::TypeFlag::PlainText) return QStringLiteral("Text"); + if (typeFlag == KBibTeX::TypeFlag::Source) return QStringLiteral("Source"); + if (typeFlag == KBibTeX::TypeFlag::Person) return QStringLiteral("Person"); + if (typeFlag == KBibTeX::TypeFlag::Keyword) return QStringLiteral("Keyword"); + if (typeFlag == KBibTeX::TypeFlag::Reference) return QStringLiteral("Reference"); + if (typeFlag == KBibTeX::TypeFlag::Verbatim) return QStringLiteral("Verbatim"); return QString(); } diff --git a/src/config/preferences-generator.py b/src/config/preferences-generator.py index cabdc8f8..21add0aa 100644 --- a/src/config/preferences-generator.py +++ b/src/config/preferences-generator.py @@ -1,504 +1,504 @@ ########################################################################### # Copyright (C) 2019 by Thomas Fischer # # # # This script 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) any later version. # # # # This script 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 script; if not, see . # ########################################################################### import sys import json import datetime def print_copyright_header(outputdevice=sys.stdout): """Print the default copyright statement to the output device.""" print("""/*************************************************************************** * Copyright (C) 2004-{year} by Thomas Fischer * * * * 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) any later version. * * * * 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 . * ***************************************************************************/""" .format(year=datetime.date.today().year), file=outputdevice) print(file=outputdevice) print("""/// This file has been automatically generated using the script 'preferences-generator.py' /// based on configuration data from file 'preferences.json'. If there are any problems or /// bugs, you need to fix those two files and re-generated both 'preferences.h' and /// 'preferences.cpp'. Manual changes in this file will be overwritten the next time the /// script will be run. You have been warned.""", file=outputdevice) print(file=outputdevice) def needsReference(type): """Check if given Qt/C++ data type should be passed by reference rather than by value.""" return type in ["QString", "QStringList"] \ or type.startswith(('QPair<', 'QVector<', 'QSet<', 'QList<', 'QLinkedList<', 'QMap<', 'QVector<', 'QHash<')) def print_header(headerincludes, implementationincludes, enums, settings, outputdevice=sys.stdout): """Print the header file for the Preferences ('preferences.h').""" print_copyright_header(outputdevice) # Include guard definition print("#ifndef KBIBTEX_CONFIG_PREFERENCES_H", file=outputdevice) print("#define KBIBTEX_CONFIG_PREFERENCES_H", file=outputdevice) print(file=outputdevice) # Include other headers as necessary for includeline in headerincludes: if len(includeline) == 0: print(file=outputdevice) elif includeline[0] == '#': print(includeline, file=outputdevice) else: print("#include", includeline, file=outputdevice) if headerincludes: print(file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print('#include "kbibtexconfig_export.h"', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print(file=outputdevice) print("class KBIBTEXCONFIG_EXPORT Preferences {", file=outputdevice) print("public:", file=outputdevice) print(" static Preferences &instance();", file=outputdevice) print(" ~Preferences();", file=outputdevice) print(file=outputdevice) for enum in sorted(enums): - print(" enum " + enum + " {", end="", file=outputdevice) + print(" enum class " + enum + " {", end="", file=outputdevice) first = True for enumvaluepair in enums[enum]: if not first: print(",", end="", file=outputdevice) print(" ", end="", file=outputdevice) if isinstance(enumvaluepair, list): print(enumvaluepair[0], end="", file=outputdevice) if len(enumvaluepair) >= 2 and enumvaluepair[1] != None: print( " = " + str(enumvaluepair[1]), end="", file=outputdevice) elif isinstance(enumvaluepair, str): print(enumvaluepair, end="", file=outputdevice) first = False print(" };", file=outputdevice) if enums: print(file=outputdevice) for setting in settings: stem = setting['stem'] type = setting['type'] lowercasestart = stem[0].lower() + stem[1:] print(file=outputdevice) print(" /// ***", stem, "of type", type, "***", file=outputdevice) print(file=outputdevice) if 'predefined' in setting: for predefined in setting['predefined']: print(" static const " + type + " " + lowercasestart + predefined[0] + ";", file=outputdevice) print(" static const " + type + " default" + stem + ";", file=outputdevice) if 'availabletype' in setting: print(" static const " + setting['availabletype'] + " available" + stem + "s;", file=outputdevice) print((" const " if needsReference(type) else " ") + type + (" &" if needsReference(type) else " ") + lowercasestart + "();", file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print(' /*!', file=outputdevice) print(' * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions', file=outputdevice) print(' */', file=outputdevice) print(" bool set" + stem + "(const " + type + (" &" if needsReference(type) else " ") + lowercasestart + ");", file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print("", file=outputdevice) print("private:", file=outputdevice) print(" Q_DISABLE_COPY(Preferences)", file=outputdevice) print(file=outputdevice) print(" explicit Preferences();", file=outputdevice) print(file=outputdevice) print(" class Private;", file=outputdevice) print(" Private *const d;", file=outputdevice) print("};", file=outputdevice) print(file=outputdevice) print("#endif // KBIBTEX_CONFIG_PREFERENCES_H", file=outputdevice) def print_implementation(headerincludes, implementationincludes, enums, settings, outputdevice=sys.stdout): """Print the implementatiom file for the Preferences ('preferences.cpp').""" print_copyright_header(outputdevice) # Include headers that will always be necessary print('#include ', file=outputdevice) print(file=outputdevice) print('#include ', file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print('#include ', file=outputdevice) print('#include ', file=outputdevice) print('#include ', file=outputdevice) print('#include ', file=outputdevice) print('#else // HAVE_KF5', file=outputdevice) print('#define i18n(text) QStringLiteral(text)', file=outputdevice) print('#define i18nc(comment,text) QStringLiteral(text)', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print(file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print('#include ', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print(file=outputdevice) # Include other headers as necessary for includeline in implementationincludes: if len(includeline) == 0: print(file=outputdevice) elif includeline[0] == '#': print(includeline, file=outputdevice) else: print("#include", includeline, file=outputdevice) if implementationincludes: print(file=outputdevice) print('class Preferences::Private', file=outputdevice) print('{', file=outputdevice) print('public:', file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print(' KSharedConfigPtr config;', file=outputdevice) print(' KConfigWatcher::Ptr watcher;', file=outputdevice) print(file=outputdevice) for setting in settings: stem = setting['stem'] type = setting['type'] if type in enums: type = "Preferences::" + type print(' bool dirtyFlag' + stem + ';', file=outputdevice) print(' ' + type + ' cached' + stem + ';', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) # Constructor for Preferences::Private print(file=outputdevice) print(' Private(Preferences *)', file=outputdevice) print(' {', file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print(' config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc"));', file=outputdevice) print(' watcher = KConfigWatcher::create(config);', file=outputdevice) for setting in settings: stem = setting['stem'] print(' dirtyFlag' + stem + ' = true;', file=outputdevice) print(' cached' + stem + ' = Preferences::default' + stem + ';', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print(' }', file=outputdevice) print(file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) first = True for setting in settings: stem = setting['stem'] type = setting['type'] if type in enums: type = "Preferences::" + type if first: first = False else: print(file=outputdevice) print(' inline bool validateValueFor' + stem + '(const ' + type + (" &" if needsReference(type) else " ") + "valueToBeChecked) {", file=outputdevice) if 'validationcode' in setting: if isinstance(setting['validationcode'], list): for line in setting['validationcode']: print(' ' + line, file=outputdevice) if not setting['validationcode'][-1].startswith("return "): print(' return false;', file=outputdevice) elif isinstance(setting['validationcode'], str): print(' ' + setting['validationcode'], file=outputdevice) elif 'availabletype' in setting and setting['availabletype'].startswith('QVectorfirst == valueToBeChecked) return true;', file=outputdevice) print(' return false;', file=outputdevice) elif 'availabletype' in setting and setting['availabletype'] == "QStringList": print(' return Preferences::available' + stem + 's.contains(valueToBeChecked);', file=outputdevice) else: print(' Q_UNUSED(valueToBeChecked)', file=outputdevice) print(' return true;', file=outputdevice) print(' }', file=outputdevice) if 'readEntry' in setting: print(file=outputdevice) print(' ' + type + ' readEntry' + stem + '(const KConfigGroup &configGroup, const QString &key) const', file=outputdevice) print(' {', file=outputdevice) if isinstance(setting['readEntry'], list): for line in setting['readEntry']: print(' ' + line, file=outputdevice) elif isinstance(setting['readEntry'], str): print(' ' + setting['readEntry'], file=outputdevice) print(' }', file=outputdevice) if 'writeEntry' in setting: print(file=outputdevice) print(' void writeEntry' + stem + '(KConfigGroup &configGroup, const QString &key, const ' + type + (" &" if needsReference(type) else " ") + 'valueToBeWritten)', file=outputdevice) print(' {', file=outputdevice) if isinstance(setting['writeEntry'], list): for line in setting['writeEntry']: print(' ' + line, file=outputdevice) elif isinstance(setting['writeEntry'], str): print(' ' + setting['writeEntry'], file=outputdevice) print(' }', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print('};', file=outputdevice) # Singleton function Preferences::instance() print(file=outputdevice) print('Preferences &Preferences::instance()', file=outputdevice) print('{', file=outputdevice) print(' static Preferences singleton;', file=outputdevice) print(' return singleton;', file=outputdevice) print('}', file=outputdevice) print(file=outputdevice) # Constructor for class Preferences print('Preferences::Preferences()', file=outputdevice) print(' : d(new Preferences::Private(this))', file=outputdevice) print('{', file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print( ' QObject::connect(d->watcher.data(), &KConfigWatcher::configChanged, QCoreApplication::instance(), [this](const KConfigGroup &group, const QByteArrayList &names) {', file=outputdevice) print(' QSet eventsToPublish;', file=outputdevice) for setting in settings: stem = setting['stem'] configgroup = setting['configgroup'] if 'configgroup' in setting else 'General' print(' if (group.name() == QStringLiteral("' + configgroup + '") && names.contains("' + stem + '")) {', file=outputdevice) print(' /// Configuration setting ' + stem + ' got changed by another Preferences instance";', file=outputdevice) print(' d->dirtyFlag' + stem + ' = true;', file=outputdevice) if 'notificationevent' in setting: print(' eventsToPublish.insert(' + setting['notificationevent'] + ');', file=outputdevice) print(' }', file=outputdevice) print(file=outputdevice) print(' for (const int eventId : eventsToPublish)', file=outputdevice) print(' NotificationHub::publishEvent(eventId);', file=outputdevice) print(' });', file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print('}', file=outputdevice) print(file=outputdevice) # Destructor for Preferences print('Preferences::~Preferences()', file=outputdevice) print('{', file=outputdevice) print(' delete d;', file=outputdevice) print('}', file=outputdevice) for setting in settings: stem = setting['stem'] configgroup = setting['configgroup'] if 'configgroup' in setting else 'General' type = setting['type'] typeInConfig = "int" if type in enums.keys() else type if 'podtype' in setting: typeInConfig = setting['podtype'] if type in enums: type = "Preferences::" + type lowercasestart = stem[0].lower() + stem[1:] print(file=outputdevice) if 'predefined' in setting: for predefined in setting['predefined']: print('const ' + type + ' Preferences::' + lowercasestart + predefined[0] + " = " + predefined[1] + ";", file=outputdevice) if 'available' in setting: available = setting['available'] print('const ' + setting['availabletype'] + ' Preferences::available' + stem + ('s ' if available.startswith("{") else "s = ") + available + ";", file=outputdevice) default = "nullptr" if 'default' in setting: default = setting['default'] if 'predefined' in setting and default in [pair[0] for pair in setting['predefined']]: default = "Preferences::" + lowercasestart + default elif 'availabletype' in setting: if setting['availabletype'].startswith("QVectordirtyFlag' + stem + ') {', file=outputdevice) print(' d->config->reparseConfiguration();', file=outputdevice) print(' static const KConfigGroup configGroup(d->config, QStringLiteral("' + configgroup + '"));', file=outputdevice) print(' const ' + type + ' valueFromConfig = ', end="", file=outputdevice) if 'readEntry' in setting: print('d->readEntry' + stem + '(configGroup, QStringLiteral("' + stem + '"));', file=outputdevice) else: if typeInConfig != type: print('static_cast<' + type + '>(', end="", file=outputdevice) print('configGroup.readEntry(QStringLiteral("' + stem + '"), ', end="", file=outputdevice) if typeInConfig != type: print('static_cast<' + typeInConfig + '>(', end="", file=outputdevice) print('Preferences::default' + stem, end="", file=outputdevice) if typeInConfig != type: print(')', end="", file=outputdevice) print(')', end="", file=outputdevice) if typeInConfig != type: print(')', end="", file=outputdevice) print(';', file=outputdevice) print(' if (d->validateValueFor' + stem + '(valueFromConfig)) {', file=outputdevice) print(' d->cached' + stem + ' = valueFromConfig;', file=outputdevice) print(' d->dirtyFlag' + stem + ' = false;', file=outputdevice) print(' } else {', file=outputdevice) print(' /// Configuration file setting for ' + stem + ' has an invalid value, using default as fallback', file=outputdevice) print(' set' + stem + '(Preferences::default' + stem + ');', file=outputdevice) print(' }', file=outputdevice) print(' }', file=outputdevice) print(' return d->cached' + stem + ";", file=outputdevice) print('#else // HAVE_KF5', file=outputdevice) print(' return default' + stem + ";", file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) print("}", file=outputdevice) print(file=outputdevice) print('#ifdef HAVE_KF5', file=outputdevice) print("bool Preferences::set" + stem + '(const ' + type + (" &" if needsReference(type) else " ") + 'newValue)', file=outputdevice) print("{", file=outputdevice) if 'sanitizecode' in setting: newValueVariable = 'sanitizedNewValue' if isinstance(setting['sanitizecode'], str): print(' ' + setting['sanitizecode'], file=outputdevice) elif isinstance(setting['sanitizecode'], list): for line in setting['sanitizecode']: print(' ' + line, file=outputdevice) else: newValueVariable = 'newValue' elif 'availabletype' in setting and setting['availabletype'] == 'QStringList': newValueVariable = 'sanitizedNewValue' print(' ' + type + ' sanitizedNewValue = newValue;', file=outputdevice) print(' const ' + type + ' lowerSanitizedNewValue = sanitizedNewValue.toLower();', file=outputdevice) print(' for (const QString &known' + stem + ' : available' + stem + 's)', file=outputdevice) print(' if (known' + stem + '.toLower() == lowerSanitizedNewValue) {', file=outputdevice) print(' sanitizedNewValue = known' + stem + ';', file=outputdevice) print(' break;', file=outputdevice) print(' }', file=outputdevice) else: newValueVariable = 'newValue' print(' if (!d->validateValueFor' + stem + '(' + newValueVariable + ')) return false;', file=outputdevice) print(' d->dirtyFlag' + stem + ' = false;', file=outputdevice) print(' d->cached' + stem + ' = ' + newValueVariable + ';', file=outputdevice) print(' static KConfigGroup configGroup(d->config, QStringLiteral("' + configgroup + '"));', file=outputdevice) print(' const ' + type + ' valueFromConfig = ', end="", file=outputdevice) if 'readEntry' in setting: print('d->readEntry' + stem + '(configGroup, QStringLiteral("' + stem + '"));', file=outputdevice) else: if typeInConfig != type: print('static_cast<' + type + '>(', end="", file=outputdevice) print('configGroup.readEntry(QStringLiteral("' + stem + '"), ', end="", file=outputdevice) if typeInConfig != type: print('static_cast<' + typeInConfig + '>(', end="", file=outputdevice) print('Preferences::default' + stem, end="", file=outputdevice) if typeInConfig != type: print(')', end="", file=outputdevice) print(')', end="", file=outputdevice) if typeInConfig != type: print(')', end="", file=outputdevice) print(';', file=outputdevice) print(' if (valueFromConfig == ' + newValueVariable + ') return false;', file=outputdevice) if 'writeEntry' in setting: print(' d->writeEntry' + stem + '(configGroup, QStringLiteral("' + stem + '"), ' + newValueVariable + ');', file=outputdevice) else: print(' configGroup.writeEntry(QStringLiteral("' + stem + '"), ', end="", file=outputdevice) if typeInConfig != type: print('static_cast<' + typeInConfig + '>(', end="", file=outputdevice) print(newValueVariable, end="", file=outputdevice) if typeInConfig != type: print(')', end="", file=outputdevice) print(', KConfig::Notify);', file=outputdevice) print(' d->config->sync();', file=outputdevice) # NotificationHub::publishEvent(notificationEventId); print(' return true;', file=outputdevice) print("}", file=outputdevice) print('#endif // HAVE_KF5', file=outputdevice) jsondata = {} with open("preferences.json") as jsonfile: jsondata = json.load(jsonfile) headerincludes = jsondata['headerincludes'] \ if 'headerincludes' in jsondata else {} implementationincludes = jsondata['implementationincludes'] \ if 'implementationincludes' in jsondata else {} enums = jsondata['enums'] \ if 'enums' in jsondata else {} settings = jsondata['settings'] \ if 'settings' in jsondata else {} for setting in settings: if 'headerincludes' in setting: headerincludes.extend(setting['headerincludes']) with open("/tmp/preferences.h", "w") as headerfile: print_header(headerincludes, implementationincludes, enums, settings, outputdevice=headerfile) with open("/tmp/preferences.cpp", "w") as implementationfile: print_implementation(headerincludes, implementationincludes, enums, settings, outputdevice=implementationfile) diff --git a/src/config/preferences.cpp b/src/config/preferences.cpp index 5da0d6d2..92f61ddc 100644 --- a/src/config/preferences.cpp +++ b/src/config/preferences.cpp @@ -1,1195 +1,1195 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 . * ***************************************************************************/ /// This file has been automatically generated using the script 'preferences-generator.py' /// based on configuration data from file 'preferences.json'. If there are any problems or /// bugs, you need to fix those two files and re-generated both 'preferences.h' and /// 'preferences.cpp'. Manual changes in this file will be overwritten the next time the /// script will be run. You have been warned. #include #include #ifdef HAVE_KF5 #include #include #include #include #else // HAVE_KF5 #define i18n(text) QStringLiteral(text) #define i18nc(comment,text) QStringLiteral(text) #endif // HAVE_KF5 #ifdef HAVE_KF5 #include #endif // HAVE_KF5 #include #include class Preferences::Private { public: #ifdef HAVE_KF5 KSharedConfigPtr config; KConfigWatcher::Ptr watcher; bool dirtyFlagBibliographySystem; Preferences::BibliographySystem cachedBibliographySystem; bool dirtyFlagPersonNameFormat; QString cachedPersonNameFormat; bool dirtyFlagCopyReferenceCommand; QString cachedCopyReferenceCommand; bool dirtyFlagPageSize; QPageSize::PageSizeId cachedPageSize; bool dirtyFlagBackupScope; Preferences::BackupScope cachedBackupScope; bool dirtyFlagNumberOfBackups; int cachedNumberOfBackups; bool dirtyFlagIdSuggestionFormatStrings; QStringList cachedIdSuggestionFormatStrings; bool dirtyFlagActiveIdSuggestionFormatString; QString cachedActiveIdSuggestionFormatString; bool dirtyFlagLyXUseAutomaticPipeDetection; bool cachedLyXUseAutomaticPipeDetection; bool dirtyFlagLyXPipePath; QString cachedLyXPipePath; bool dirtyFlagBibTeXEncoding; QString cachedBibTeXEncoding; bool dirtyFlagBibTeXStringDelimiter; QString cachedBibTeXStringDelimiter; bool dirtyFlagBibTeXQuoteComment; Preferences::QuoteComment cachedBibTeXQuoteComment; bool dirtyFlagBibTeXKeywordCasing; KBibTeX::Casing cachedBibTeXKeywordCasing; bool dirtyFlagBibTeXProtectCasing; bool cachedBibTeXProtectCasing; bool dirtyFlagBibTeXListSeparator; QString cachedBibTeXListSeparator; bool dirtyFlagLaTeXBabelLanguage; QString cachedLaTeXBabelLanguage; bool dirtyFlagBibTeXBibliographyStyle; QString cachedBibTeXBibliographyStyle; bool dirtyFlagFileViewDoubleClickAction; Preferences::FileViewDoubleClickAction cachedFileViewDoubleClickAction; bool dirtyFlagColorCodes; QVector> cachedColorCodes; #endif // HAVE_KF5 Private(Preferences *) { #ifdef HAVE_KF5 config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); watcher = KConfigWatcher::create(config); dirtyFlagBibliographySystem = true; cachedBibliographySystem = Preferences::defaultBibliographySystem; dirtyFlagPersonNameFormat = true; cachedPersonNameFormat = Preferences::defaultPersonNameFormat; dirtyFlagCopyReferenceCommand = true; cachedCopyReferenceCommand = Preferences::defaultCopyReferenceCommand; dirtyFlagPageSize = true; cachedPageSize = Preferences::defaultPageSize; dirtyFlagBackupScope = true; cachedBackupScope = Preferences::defaultBackupScope; dirtyFlagNumberOfBackups = true; cachedNumberOfBackups = Preferences::defaultNumberOfBackups; dirtyFlagIdSuggestionFormatStrings = true; cachedIdSuggestionFormatStrings = Preferences::defaultIdSuggestionFormatStrings; dirtyFlagActiveIdSuggestionFormatString = true; cachedActiveIdSuggestionFormatString = Preferences::defaultActiveIdSuggestionFormatString; dirtyFlagLyXUseAutomaticPipeDetection = true; cachedLyXUseAutomaticPipeDetection = Preferences::defaultLyXUseAutomaticPipeDetection; dirtyFlagLyXPipePath = true; cachedLyXPipePath = Preferences::defaultLyXPipePath; dirtyFlagBibTeXEncoding = true; cachedBibTeXEncoding = Preferences::defaultBibTeXEncoding; dirtyFlagBibTeXStringDelimiter = true; cachedBibTeXStringDelimiter = Preferences::defaultBibTeXStringDelimiter; dirtyFlagBibTeXQuoteComment = true; cachedBibTeXQuoteComment = Preferences::defaultBibTeXQuoteComment; dirtyFlagBibTeXKeywordCasing = true; cachedBibTeXKeywordCasing = Preferences::defaultBibTeXKeywordCasing; dirtyFlagBibTeXProtectCasing = true; cachedBibTeXProtectCasing = Preferences::defaultBibTeXProtectCasing; dirtyFlagBibTeXListSeparator = true; cachedBibTeXListSeparator = Preferences::defaultBibTeXListSeparator; dirtyFlagLaTeXBabelLanguage = true; cachedLaTeXBabelLanguage = Preferences::defaultLaTeXBabelLanguage; dirtyFlagBibTeXBibliographyStyle = true; cachedBibTeXBibliographyStyle = Preferences::defaultBibTeXBibliographyStyle; dirtyFlagFileViewDoubleClickAction = true; cachedFileViewDoubleClickAction = Preferences::defaultFileViewDoubleClickAction; dirtyFlagColorCodes = true; cachedColorCodes = Preferences::defaultColorCodes; #endif // HAVE_KF5 } #ifdef HAVE_KF5 inline bool validateValueForBibliographySystem(const Preferences::BibliographySystem valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availableBibliographySystems.constBegin(); it != Preferences::availableBibliographySystems.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForPersonNameFormat(const QString &valueToBeChecked) { return valueToBeChecked.contains(QStringLiteral("%f")) && valueToBeChecked.contains(QStringLiteral("%l")) && valueToBeChecked.contains(QStringLiteral("%s")); } inline bool validateValueForCopyReferenceCommand(const QString &valueToBeChecked) { return Preferences::availableCopyReferenceCommands.contains(valueToBeChecked); } inline bool validateValueForPageSize(const QPageSize::PageSizeId valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availablePageSizes.constBegin(); it != Preferences::availablePageSizes.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForBackupScope(const Preferences::BackupScope valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availableBackupScopes.constBegin(); it != Preferences::availableBackupScopes.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForNumberOfBackups(const int valueToBeChecked) { return valueToBeChecked >= 0; } inline bool validateValueForIdSuggestionFormatStrings(const QStringList &valueToBeChecked) { return !valueToBeChecked.isEmpty() && (valueToBeChecked.front().contains(QLatin1Char('A')) || valueToBeChecked.front().contains(QLatin1Char('a')) || valueToBeChecked.front().contains(QLatin1Char('y')) || valueToBeChecked.front().contains(QLatin1Char('Y')) || valueToBeChecked.front().contains(QLatin1Char('T'))); } inline bool validateValueForActiveIdSuggestionFormatString(const QString &valueToBeChecked) { Q_UNUSED(valueToBeChecked) return true; } inline bool validateValueForLyXUseAutomaticPipeDetection(const bool valueToBeChecked) { Q_UNUSED(valueToBeChecked) return true; } inline bool validateValueForLyXPipePath(const QString &valueToBeChecked) { return valueToBeChecked.startsWith(QLatin1Char('/')); } inline bool validateValueForBibTeXEncoding(const QString &valueToBeChecked) { return Preferences::availableBibTeXEncodings.contains(valueToBeChecked); } inline bool validateValueForBibTeXStringDelimiter(const QString &valueToBeChecked) { return Preferences::availableBibTeXStringDelimiters.contains(valueToBeChecked); } inline bool validateValueForBibTeXQuoteComment(const Preferences::QuoteComment valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availableBibTeXQuoteComments.constBegin(); it != Preferences::availableBibTeXQuoteComments.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForBibTeXKeywordCasing(const KBibTeX::Casing valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availableBibTeXKeywordCasings.constBegin(); it != Preferences::availableBibTeXKeywordCasings.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForBibTeXProtectCasing(const bool valueToBeChecked) { Q_UNUSED(valueToBeChecked) return true; } inline bool validateValueForBibTeXListSeparator(const QString &valueToBeChecked) { return Preferences::availableBibTeXListSeparators.contains(valueToBeChecked); } inline bool validateValueForLaTeXBabelLanguage(const QString &valueToBeChecked) { return !valueToBeChecked.isEmpty(); } inline bool validateValueForBibTeXBibliographyStyle(const QString &valueToBeChecked) { return !valueToBeChecked.isEmpty(); } inline bool validateValueForFileViewDoubleClickAction(const Preferences::FileViewDoubleClickAction valueToBeChecked) { for (QVector>::ConstIterator it = Preferences::availableFileViewDoubleClickActions.constBegin(); it != Preferences::availableFileViewDoubleClickActions.constEnd(); ++it) if (it->first == valueToBeChecked) return true; return false; } inline bool validateValueForColorCodes(const QVector> &valueToBeChecked) { static const QColor white(Qt::white), black(Qt::black); for (QVector>::ConstIterator it = valueToBeChecked.constBegin(); it != valueToBeChecked.constEnd(); ++it) if (it->first == white || it->first == black || it->second.isEmpty()) return false; return true; } QVector> readEntryColorCodes(const KConfigGroup &configGroup, const QString &key) const { const QString rawEntry = configGroup.readEntry(key, QString()); if (rawEntry.isEmpty()) return Preferences::defaultColorCodes; const QStringList pairs = rawEntry.split(QStringLiteral("\0\0"), QString::SkipEmptyParts); if (pairs.isEmpty()) return Preferences::defaultColorCodes; QVector> result; for (const QString &pair : pairs) { const QStringList colorLabelPair = pair.split(QStringLiteral("\0"), QString::SkipEmptyParts); if (colorLabelPair.length() != 2) return Preferences::defaultColorCodes; result.append(qMakePair(QColor(colorLabelPair[0]), colorLabelPair[1])); } return result; } void writeEntryColorCodes(KConfigGroup &configGroup, const QString &key, const QVector> &valueToBeWritten) { QString rawEntry; for (QVector>::ConstIterator it = valueToBeWritten.constBegin(); it != valueToBeWritten.constEnd(); ++it) { if (!rawEntry.isEmpty()) rawEntry.append(QStringLiteral("\0\0")); rawEntry = rawEntry.append(it->first.name(QColor::HexRgb)).append(QStringLiteral("\0")).append(it->second); } configGroup.writeEntry(key, rawEntry, KConfig::Notify); } #endif // HAVE_KF5 }; Preferences &Preferences::instance() { static Preferences singleton; return singleton; } Preferences::Preferences() : d(new Preferences::Private(this)) { #ifdef HAVE_KF5 QObject::connect(d->watcher.data(), &KConfigWatcher::configChanged, QCoreApplication::instance(), [this](const KConfigGroup &group, const QByteArrayList &names) { QSet eventsToPublish; if (group.name() == QStringLiteral("General") && names.contains("BibliographySystem")) { /// Configuration setting BibliographySystem got changed by another Preferences instance"; d->dirtyFlagBibliographySystem = true; eventsToPublish.insert(NotificationHub::EventBibliographySystemChanged); } if (group.name() == QStringLiteral("General") && names.contains("PersonNameFormat")) { /// Configuration setting PersonNameFormat got changed by another Preferences instance"; d->dirtyFlagPersonNameFormat = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("LaTeX") && names.contains("CopyReferenceCommand")) { /// Configuration setting CopyReferenceCommand got changed by another Preferences instance"; d->dirtyFlagCopyReferenceCommand = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("General") && names.contains("PageSize")) { /// Configuration setting PageSize got changed by another Preferences instance"; d->dirtyFlagPageSize = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("InputOutput") && names.contains("BackupScope")) { /// Configuration setting BackupScope got changed by another Preferences instance"; d->dirtyFlagBackupScope = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("InputOutput") && names.contains("NumberOfBackups")) { /// Configuration setting NumberOfBackups got changed by another Preferences instance"; d->dirtyFlagNumberOfBackups = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("IdSuggestions") && names.contains("IdSuggestionFormatStrings")) { /// Configuration setting IdSuggestionFormatStrings got changed by another Preferences instance"; d->dirtyFlagIdSuggestionFormatStrings = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("IdSuggestions") && names.contains("ActiveIdSuggestionFormatString")) { /// Configuration setting ActiveIdSuggestionFormatString got changed by another Preferences instance"; d->dirtyFlagActiveIdSuggestionFormatString = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("LyX") && names.contains("LyXUseAutomaticPipeDetection")) { /// Configuration setting LyXUseAutomaticPipeDetection got changed by another Preferences instance"; d->dirtyFlagLyXUseAutomaticPipeDetection = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("LyX") && names.contains("LyXPipePath")) { /// Configuration setting LyXPipePath got changed by another Preferences instance"; d->dirtyFlagLyXPipePath = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXEncoding")) { /// Configuration setting BibTeXEncoding got changed by another Preferences instance"; d->dirtyFlagBibTeXEncoding = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXStringDelimiter")) { /// Configuration setting BibTeXStringDelimiter got changed by another Preferences instance"; d->dirtyFlagBibTeXStringDelimiter = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXQuoteComment")) { /// Configuration setting BibTeXQuoteComment got changed by another Preferences instance"; d->dirtyFlagBibTeXQuoteComment = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXKeywordCasing")) { /// Configuration setting BibTeXKeywordCasing got changed by another Preferences instance"; d->dirtyFlagBibTeXKeywordCasing = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXProtectCasing")) { /// Configuration setting BibTeXProtectCasing got changed by another Preferences instance"; d->dirtyFlagBibTeXProtectCasing = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterBibTeX") && names.contains("BibTeXListSeparator")) { /// Configuration setting BibTeXListSeparator got changed by another Preferences instance"; d->dirtyFlagBibTeXListSeparator = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterLaTeXbased") && names.contains("LaTeXBabelLanguage")) { /// Configuration setting LaTeXBabelLanguage got changed by another Preferences instance"; d->dirtyFlagLaTeXBabelLanguage = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("FileExporterLaTeXbased") && names.contains("BibTeXBibliographyStyle")) { /// Configuration setting BibTeXBibliographyStyle got changed by another Preferences instance"; d->dirtyFlagBibTeXBibliographyStyle = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("User Interface") && names.contains("FileViewDoubleClickAction")) { /// Configuration setting FileViewDoubleClickAction got changed by another Preferences instance"; d->dirtyFlagFileViewDoubleClickAction = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } if (group.name() == QStringLiteral("Color Labels") && names.contains("ColorCodes")) { /// Configuration setting ColorCodes got changed by another Preferences instance"; d->dirtyFlagColorCodes = true; eventsToPublish.insert(NotificationHub::EventConfigurationChanged); } for (const int eventId : eventsToPublish) NotificationHub::publishEvent(eventId); }); #endif // HAVE_KF5 } Preferences::~Preferences() { delete d; } -const QVector> Preferences::availableBibliographySystems {{Preferences::BibTeX, i18n("BibTeX")}, {Preferences::BibLaTeX, i18n("BibLaTeX")}}; +const QVector> Preferences::availableBibliographySystems {{Preferences::BibliographySystem::BibTeX, i18n("BibTeX")}, {Preferences::BibliographySystem::BibLaTeX, i18n("BibLaTeX")}}; const Preferences::BibliographySystem Preferences::defaultBibliographySystem = Preferences::availableBibliographySystems.front().first; Preferences::BibliographySystem Preferences::bibliographySystem() { #ifdef HAVE_KF5 if (d->dirtyFlagBibliographySystem) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("General")); const Preferences::BibliographySystem valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibliographySystem"), static_cast(Preferences::defaultBibliographySystem))); if (d->validateValueForBibliographySystem(valueFromConfig)) { d->cachedBibliographySystem = valueFromConfig; d->dirtyFlagBibliographySystem = false; } else { /// Configuration file setting for BibliographySystem has an invalid value, using default as fallback setBibliographySystem(Preferences::defaultBibliographySystem); } } return d->cachedBibliographySystem; #else // HAVE_KF5 return defaultBibliographySystem; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibliographySystem(const Preferences::BibliographySystem newValue) { if (!d->validateValueForBibliographySystem(newValue)) return false; d->dirtyFlagBibliographySystem = false; d->cachedBibliographySystem = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("General")); const Preferences::BibliographySystem valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibliographySystem"), static_cast(Preferences::defaultBibliographySystem))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BibliographySystem"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QString Preferences::personNameFormatLastFirst = QStringLiteral("<%l><, %s><, %f>"); const QString Preferences::personNameFormatFirstLast = QStringLiteral("<%f ><%l>< %s>"); const QString Preferences::defaultPersonNameFormat = Preferences::personNameFormatLastFirst; const QString &Preferences::personNameFormat() { #ifdef HAVE_KF5 if (d->dirtyFlagPersonNameFormat) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("General")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("PersonNameFormat"), Preferences::defaultPersonNameFormat); if (d->validateValueForPersonNameFormat(valueFromConfig)) { d->cachedPersonNameFormat = valueFromConfig; d->dirtyFlagPersonNameFormat = false; } else { /// Configuration file setting for PersonNameFormat has an invalid value, using default as fallback setPersonNameFormat(Preferences::defaultPersonNameFormat); } } return d->cachedPersonNameFormat; #else // HAVE_KF5 return defaultPersonNameFormat; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setPersonNameFormat(const QString &newValue) { if (!d->validateValueForPersonNameFormat(newValue)) return false; d->dirtyFlagPersonNameFormat = false; d->cachedPersonNameFormat = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("General")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("PersonNameFormat"), Preferences::defaultPersonNameFormat); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("PersonNameFormat"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QStringList Preferences::availableCopyReferenceCommands {QStringLiteral("cite"), QStringLiteral("citealt"), QStringLiteral("citeauthor"), QStringLiteral("citeauthor*"), QStringLiteral("citeyear"), QStringLiteral("citeyearpar"), QStringLiteral("shortcite"), QStringLiteral("citet"), QStringLiteral("citet*"), QStringLiteral("citep"), QStringLiteral("citep*")}; const QString Preferences::defaultCopyReferenceCommand = Preferences::availableCopyReferenceCommands.front(); const QString &Preferences::copyReferenceCommand() { #ifdef HAVE_KF5 if (d->dirtyFlagCopyReferenceCommand) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("LaTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("CopyReferenceCommand"), Preferences::defaultCopyReferenceCommand); if (d->validateValueForCopyReferenceCommand(valueFromConfig)) { d->cachedCopyReferenceCommand = valueFromConfig; d->dirtyFlagCopyReferenceCommand = false; } else { /// Configuration file setting for CopyReferenceCommand has an invalid value, using default as fallback setCopyReferenceCommand(Preferences::defaultCopyReferenceCommand); } } return d->cachedCopyReferenceCommand; #else // HAVE_KF5 return defaultCopyReferenceCommand; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setCopyReferenceCommand(const QString &newValue) { QString sanitizedNewValue = newValue; const QString lowerSanitizedNewValue = sanitizedNewValue.toLower(); for (const QString &knownCopyReferenceCommand : availableCopyReferenceCommands) if (knownCopyReferenceCommand.toLower() == lowerSanitizedNewValue) { sanitizedNewValue = knownCopyReferenceCommand; break; } if (!d->validateValueForCopyReferenceCommand(sanitizedNewValue)) return false; d->dirtyFlagCopyReferenceCommand = false; d->cachedCopyReferenceCommand = sanitizedNewValue; static KConfigGroup configGroup(d->config, QStringLiteral("LaTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("CopyReferenceCommand"), Preferences::defaultCopyReferenceCommand); if (valueFromConfig == sanitizedNewValue) return false; configGroup.writeEntry(QStringLiteral("CopyReferenceCommand"), sanitizedNewValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QVector> Preferences::availablePageSizes {{QPageSize::A4, QStringLiteral("a4paper")}, {QPageSize::Letter, QStringLiteral("letter")}, {QPageSize::Legal, QStringLiteral("legal")}}; const QPageSize::PageSizeId Preferences::defaultPageSize = Preferences::availablePageSizes.front().first; QPageSize::PageSizeId Preferences::pageSize() { #ifdef HAVE_KF5 if (d->dirtyFlagPageSize) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("General")); const QPageSize::PageSizeId valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("PageSize"), static_cast(Preferences::defaultPageSize))); if (d->validateValueForPageSize(valueFromConfig)) { d->cachedPageSize = valueFromConfig; d->dirtyFlagPageSize = false; } else { /// Configuration file setting for PageSize has an invalid value, using default as fallback setPageSize(Preferences::defaultPageSize); } } return d->cachedPageSize; #else // HAVE_KF5 return defaultPageSize; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setPageSize(const QPageSize::PageSizeId newValue) { if (!d->validateValueForPageSize(newValue)) return false; d->dirtyFlagPageSize = false; d->cachedPageSize = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("General")); const QPageSize::PageSizeId valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("PageSize"), static_cast(Preferences::defaultPageSize))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("PageSize"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 -const QVector> Preferences::availableBackupScopes {{Preferences::NoBackup, i18n("No backups")}, {Preferences::LocalOnly, i18n("Local files only")}, {Preferences::BothLocalAndRemote, i18n("Both local and remote files")}}; +const QVector> Preferences::availableBackupScopes {{Preferences::BackupScope::None, i18n("No backups")}, {Preferences::BackupScope::LocalOnly, i18n("Local files only")}, {Preferences::BackupScope::BothLocalAndRemote, i18n("Both local and remote files")}}; const Preferences::BackupScope Preferences::defaultBackupScope = Preferences::availableBackupScopes.front().first; Preferences::BackupScope Preferences::backupScope() { #ifdef HAVE_KF5 if (d->dirtyFlagBackupScope) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("InputOutput")); const Preferences::BackupScope valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BackupScope"), static_cast(Preferences::defaultBackupScope))); if (d->validateValueForBackupScope(valueFromConfig)) { d->cachedBackupScope = valueFromConfig; d->dirtyFlagBackupScope = false; } else { /// Configuration file setting for BackupScope has an invalid value, using default as fallback setBackupScope(Preferences::defaultBackupScope); } } return d->cachedBackupScope; #else // HAVE_KF5 return defaultBackupScope; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBackupScope(const Preferences::BackupScope newValue) { if (!d->validateValueForBackupScope(newValue)) return false; d->dirtyFlagBackupScope = false; d->cachedBackupScope = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("InputOutput")); const Preferences::BackupScope valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BackupScope"), static_cast(Preferences::defaultBackupScope))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BackupScope"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const int Preferences::defaultNumberOfBackups = 5; int Preferences::numberOfBackups() { #ifdef HAVE_KF5 if (d->dirtyFlagNumberOfBackups) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("InputOutput")); const int valueFromConfig = configGroup.readEntry(QStringLiteral("NumberOfBackups"), Preferences::defaultNumberOfBackups); if (d->validateValueForNumberOfBackups(valueFromConfig)) { d->cachedNumberOfBackups = valueFromConfig; d->dirtyFlagNumberOfBackups = false; } else { /// Configuration file setting for NumberOfBackups has an invalid value, using default as fallback setNumberOfBackups(Preferences::defaultNumberOfBackups); } } return d->cachedNumberOfBackups; #else // HAVE_KF5 return defaultNumberOfBackups; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setNumberOfBackups(const int newValue) { if (!d->validateValueForNumberOfBackups(newValue)) return false; d->dirtyFlagNumberOfBackups = false; d->cachedNumberOfBackups = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("InputOutput")); const int valueFromConfig = configGroup.readEntry(QStringLiteral("NumberOfBackups"), Preferences::defaultNumberOfBackups); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("NumberOfBackups"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QStringList Preferences::defaultIdSuggestionFormatStrings {QStringLiteral("A"), QStringLiteral("A2|y"), QStringLiteral("A3|y"), QStringLiteral("A4|y|\":|T5"), QStringLiteral("al|\":|T"), QStringLiteral("al|y"), QStringLiteral("al|Y"), QStringLiteral("Al\"-|\"-|y"), QStringLiteral("Al\"+|Y"), QStringLiteral("al|y|T"), QStringLiteral("al|Y|T3"), QStringLiteral("al|Y|T3l"), QStringLiteral("a|\":|Y|\":|T1"), QStringLiteral("a|y"), QStringLiteral("A|\":|Y")}; const QStringList &Preferences::idSuggestionFormatStrings() { #ifdef HAVE_KF5 if (d->dirtyFlagIdSuggestionFormatStrings) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("IdSuggestions")); const QStringList valueFromConfig = configGroup.readEntry(QStringLiteral("IdSuggestionFormatStrings"), Preferences::defaultIdSuggestionFormatStrings); if (d->validateValueForIdSuggestionFormatStrings(valueFromConfig)) { d->cachedIdSuggestionFormatStrings = valueFromConfig; d->dirtyFlagIdSuggestionFormatStrings = false; } else { /// Configuration file setting for IdSuggestionFormatStrings has an invalid value, using default as fallback setIdSuggestionFormatStrings(Preferences::defaultIdSuggestionFormatStrings); } } return d->cachedIdSuggestionFormatStrings; #else // HAVE_KF5 return defaultIdSuggestionFormatStrings; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setIdSuggestionFormatStrings(const QStringList &newValue) { if (!d->validateValueForIdSuggestionFormatStrings(newValue)) return false; d->dirtyFlagIdSuggestionFormatStrings = false; d->cachedIdSuggestionFormatStrings = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("IdSuggestions")); const QStringList valueFromConfig = configGroup.readEntry(QStringLiteral("IdSuggestionFormatStrings"), Preferences::defaultIdSuggestionFormatStrings); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("IdSuggestionFormatStrings"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QString Preferences::defaultActiveIdSuggestionFormatString {}; const QString &Preferences::activeIdSuggestionFormatString() { #ifdef HAVE_KF5 if (d->dirtyFlagActiveIdSuggestionFormatString) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("IdSuggestions")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("ActiveIdSuggestionFormatString"), Preferences::defaultActiveIdSuggestionFormatString); if (d->validateValueForActiveIdSuggestionFormatString(valueFromConfig)) { d->cachedActiveIdSuggestionFormatString = valueFromConfig; d->dirtyFlagActiveIdSuggestionFormatString = false; } else { /// Configuration file setting for ActiveIdSuggestionFormatString has an invalid value, using default as fallback setActiveIdSuggestionFormatString(Preferences::defaultActiveIdSuggestionFormatString); } } return d->cachedActiveIdSuggestionFormatString; #else // HAVE_KF5 return defaultActiveIdSuggestionFormatString; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setActiveIdSuggestionFormatString(const QString &newValue) { if (!d->validateValueForActiveIdSuggestionFormatString(newValue)) return false; d->dirtyFlagActiveIdSuggestionFormatString = false; d->cachedActiveIdSuggestionFormatString = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("IdSuggestions")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("ActiveIdSuggestionFormatString"), Preferences::defaultActiveIdSuggestionFormatString); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("ActiveIdSuggestionFormatString"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const bool Preferences::defaultLyXUseAutomaticPipeDetection = true; bool Preferences::lyXUseAutomaticPipeDetection() { #ifdef HAVE_KF5 if (d->dirtyFlagLyXUseAutomaticPipeDetection) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("LyX")); const bool valueFromConfig = configGroup.readEntry(QStringLiteral("LyXUseAutomaticPipeDetection"), Preferences::defaultLyXUseAutomaticPipeDetection); if (d->validateValueForLyXUseAutomaticPipeDetection(valueFromConfig)) { d->cachedLyXUseAutomaticPipeDetection = valueFromConfig; d->dirtyFlagLyXUseAutomaticPipeDetection = false; } else { /// Configuration file setting for LyXUseAutomaticPipeDetection has an invalid value, using default as fallback setLyXUseAutomaticPipeDetection(Preferences::defaultLyXUseAutomaticPipeDetection); } } return d->cachedLyXUseAutomaticPipeDetection; #else // HAVE_KF5 return defaultLyXUseAutomaticPipeDetection; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setLyXUseAutomaticPipeDetection(const bool newValue) { if (!d->validateValueForLyXUseAutomaticPipeDetection(newValue)) return false; d->dirtyFlagLyXUseAutomaticPipeDetection = false; d->cachedLyXUseAutomaticPipeDetection = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("LyX")); const bool valueFromConfig = configGroup.readEntry(QStringLiteral("LyXUseAutomaticPipeDetection"), Preferences::defaultLyXUseAutomaticPipeDetection); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("LyXUseAutomaticPipeDetection"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QString Preferences::defaultLyXPipePath = QDir::homePath() + QStringLiteral("/.lyxpipe.in"); const QString &Preferences::lyXPipePath() { #ifdef HAVE_KF5 if (d->dirtyFlagLyXPipePath) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("LyX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("LyXPipePath"), Preferences::defaultLyXPipePath); if (d->validateValueForLyXPipePath(valueFromConfig)) { d->cachedLyXPipePath = valueFromConfig; d->dirtyFlagLyXPipePath = false; } else { /// Configuration file setting for LyXPipePath has an invalid value, using default as fallback setLyXPipePath(Preferences::defaultLyXPipePath); } } return d->cachedLyXPipePath; #else // HAVE_KF5 return defaultLyXPipePath; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setLyXPipePath(const QString &newValue) { QString sanitizedNewValue = newValue; if (sanitizedNewValue.endsWith(QStringLiteral(".out"))) sanitizedNewValue = sanitizedNewValue.left(sanitizedNewValue.length() - 4); if (!sanitizedNewValue.endsWith(QStringLiteral(".in"))) sanitizedNewValue = sanitizedNewValue.append(QStringLiteral(".in")); if (!d->validateValueForLyXPipePath(sanitizedNewValue)) return false; d->dirtyFlagLyXPipePath = false; d->cachedLyXPipePath = sanitizedNewValue; static KConfigGroup configGroup(d->config, QStringLiteral("LyX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("LyXPipePath"), Preferences::defaultLyXPipePath); if (valueFromConfig == sanitizedNewValue) return false; configGroup.writeEntry(QStringLiteral("LyXPipePath"), sanitizedNewValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QStringList Preferences::availableBibTeXEncodings {QStringLiteral("LaTeX"), QStringLiteral("US-ASCII"), QStringLiteral("ISO-8859-1"), QStringLiteral("ISO-8859-2"), QStringLiteral("ISO-8859-3"), QStringLiteral("ISO-8859-4"), QStringLiteral("ISO-8859-5"), QStringLiteral("ISO-8859-6"), QStringLiteral("ISO-8859-7"), QStringLiteral("ISO-8859-8"), QStringLiteral("ISO-8859-9"), QStringLiteral("ISO-8859-10"), QStringLiteral("ISO-8859-13"), QStringLiteral("ISO-8859-14"), QStringLiteral("ISO-8859-15"), QStringLiteral("ISO-8859-16"), QStringLiteral("UTF-8"), QStringLiteral("UTF-16"), QStringLiteral("UTF-16BE"), QStringLiteral("UTF-16LE"), QStringLiteral("UTF-32"), QStringLiteral("UTF-32BE"), QStringLiteral("UTF-32LE"), QStringLiteral("KOI8-R"), QStringLiteral("KOI8-U"), QStringLiteral("Big5"), QStringLiteral("Big5-HKSCS"), QStringLiteral("GB18030"), QStringLiteral("EUC-JP"), QStringLiteral("EUC-KR"), QStringLiteral("ISO 2022-JP"), QStringLiteral("Shift-JIS"), QStringLiteral("Windows-1250"), QStringLiteral("Windows-1251"), QStringLiteral("Windows-1252"), QStringLiteral("Windows-1253"), QStringLiteral("Windows-1254"), QStringLiteral("Windows-1255"), QStringLiteral("Windows-1256"), QStringLiteral("Windows-1257"), QStringLiteral("Windows-1258")}; const QString Preferences::defaultBibTeXEncoding = QStringLiteral("UTF-8"); const QString &Preferences::bibTeXEncoding() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXEncoding) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXEncoding"), Preferences::defaultBibTeXEncoding); if (d->validateValueForBibTeXEncoding(valueFromConfig)) { d->cachedBibTeXEncoding = valueFromConfig; d->dirtyFlagBibTeXEncoding = false; } else { /// Configuration file setting for BibTeXEncoding has an invalid value, using default as fallback setBibTeXEncoding(Preferences::defaultBibTeXEncoding); } } return d->cachedBibTeXEncoding; #else // HAVE_KF5 return defaultBibTeXEncoding; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXEncoding(const QString &newValue) { QString sanitizedNewValue = newValue; const QString lowerSanitizedNewValue = sanitizedNewValue.toLower(); for (const QString &knownBibTeXEncoding : availableBibTeXEncodings) if (knownBibTeXEncoding.toLower() == lowerSanitizedNewValue) { sanitizedNewValue = knownBibTeXEncoding; break; } if (!d->validateValueForBibTeXEncoding(sanitizedNewValue)) return false; d->dirtyFlagBibTeXEncoding = false; d->cachedBibTeXEncoding = sanitizedNewValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXEncoding"), Preferences::defaultBibTeXEncoding); if (valueFromConfig == sanitizedNewValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXEncoding"), sanitizedNewValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QStringList Preferences::availableBibTeXStringDelimiters {QStringLiteral("{}"), QStringLiteral("\"\""), QStringLiteral("()")}; const QString Preferences::defaultBibTeXStringDelimiter = Preferences::availableBibTeXStringDelimiters.front(); const QString &Preferences::bibTeXStringDelimiter() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXStringDelimiter) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXStringDelimiter"), Preferences::defaultBibTeXStringDelimiter); if (d->validateValueForBibTeXStringDelimiter(valueFromConfig)) { d->cachedBibTeXStringDelimiter = valueFromConfig; d->dirtyFlagBibTeXStringDelimiter = false; } else { /// Configuration file setting for BibTeXStringDelimiter has an invalid value, using default as fallback setBibTeXStringDelimiter(Preferences::defaultBibTeXStringDelimiter); } } return d->cachedBibTeXStringDelimiter; #else // HAVE_KF5 return defaultBibTeXStringDelimiter; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXStringDelimiter(const QString &newValue) { QString sanitizedNewValue = newValue; const QString lowerSanitizedNewValue = sanitizedNewValue.toLower(); for (const QString &knownBibTeXStringDelimiter : availableBibTeXStringDelimiters) if (knownBibTeXStringDelimiter.toLower() == lowerSanitizedNewValue) { sanitizedNewValue = knownBibTeXStringDelimiter; break; } if (!d->validateValueForBibTeXStringDelimiter(sanitizedNewValue)) return false; d->dirtyFlagBibTeXStringDelimiter = false; d->cachedBibTeXStringDelimiter = sanitizedNewValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXStringDelimiter"), Preferences::defaultBibTeXStringDelimiter); if (valueFromConfig == sanitizedNewValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXStringDelimiter"), sanitizedNewValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 -const QVector> Preferences::availableBibTeXQuoteComments {{Preferences::qcNone, i18nc("Comment Quoting", "None")}, {Preferences::qcCommand, i18nc("Comment Quoting", "@comment{\342\200\246}")}, {Preferences::qcPercentSign, i18nc("Comment Quoting", "% \342\200\246")}}; +const QVector> Preferences::availableBibTeXQuoteComments {{Preferences::QuoteComment::None, i18nc("Comment Quoting", "None")}, {Preferences::QuoteComment::Command, i18nc("Comment Quoting", "@comment{\342\200\246}")}, {Preferences::QuoteComment::PercentSign, i18nc("Comment Quoting", "% \342\200\246")}}; const Preferences::QuoteComment Preferences::defaultBibTeXQuoteComment = Preferences::availableBibTeXQuoteComments.front().first; Preferences::QuoteComment Preferences::bibTeXQuoteComment() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXQuoteComment) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const Preferences::QuoteComment valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibTeXQuoteComment"), static_cast(Preferences::defaultBibTeXQuoteComment))); if (d->validateValueForBibTeXQuoteComment(valueFromConfig)) { d->cachedBibTeXQuoteComment = valueFromConfig; d->dirtyFlagBibTeXQuoteComment = false; } else { /// Configuration file setting for BibTeXQuoteComment has an invalid value, using default as fallback setBibTeXQuoteComment(Preferences::defaultBibTeXQuoteComment); } } return d->cachedBibTeXQuoteComment; #else // HAVE_KF5 return defaultBibTeXQuoteComment; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXQuoteComment(const Preferences::QuoteComment newValue) { if (!d->validateValueForBibTeXQuoteComment(newValue)) return false; d->dirtyFlagBibTeXQuoteComment = false; d->cachedBibTeXQuoteComment = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const Preferences::QuoteComment valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibTeXQuoteComment"), static_cast(Preferences::defaultBibTeXQuoteComment))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXQuoteComment"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 -const QVector> Preferences::availableBibTeXKeywordCasings {{KBibTeX::cLowerCase, i18nc("Casing of strings", "lowercase")}, {KBibTeX::cInitialCapital, i18nc("Casing of strings", "Initial capital")}, {KBibTeX::cUpperCamelCase, i18nc("Casing of strings", "UpperCamelCase")}, {KBibTeX::cLowerCamelCase, i18nc("Casing of strings", "lowerCamelCase")}, {KBibTeX::cUpperCase, i18nc("Casing of strings", "UPPERCASE")}}; +const QVector> Preferences::availableBibTeXKeywordCasings {{KBibTeX::Casing::LowerCase, i18nc("Casing of strings", "lowercase")}, {KBibTeX::Casing::InitialCapital, i18nc("Casing of strings", "Initial capital")}, {KBibTeX::Casing::UpperCamelCase, i18nc("Casing of strings", "UpperCamelCase")}, {KBibTeX::Casing::LowerCamelCase, i18nc("Casing of strings", "lowerCamelCase")}, {KBibTeX::Casing::UpperCase, i18nc("Casing of strings", "UPPERCASE")}}; const KBibTeX::Casing Preferences::defaultBibTeXKeywordCasing = Preferences::availableBibTeXKeywordCasings.front().first; KBibTeX::Casing Preferences::bibTeXKeywordCasing() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXKeywordCasing) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const KBibTeX::Casing valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibTeXKeywordCasing"), static_cast(Preferences::defaultBibTeXKeywordCasing))); if (d->validateValueForBibTeXKeywordCasing(valueFromConfig)) { d->cachedBibTeXKeywordCasing = valueFromConfig; d->dirtyFlagBibTeXKeywordCasing = false; } else { /// Configuration file setting for BibTeXKeywordCasing has an invalid value, using default as fallback setBibTeXKeywordCasing(Preferences::defaultBibTeXKeywordCasing); } } return d->cachedBibTeXKeywordCasing; #else // HAVE_KF5 return defaultBibTeXKeywordCasing; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXKeywordCasing(const KBibTeX::Casing newValue) { if (!d->validateValueForBibTeXKeywordCasing(newValue)) return false; d->dirtyFlagBibTeXKeywordCasing = false; d->cachedBibTeXKeywordCasing = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const KBibTeX::Casing valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("BibTeXKeywordCasing"), static_cast(Preferences::defaultBibTeXKeywordCasing))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXKeywordCasing"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const bool Preferences::defaultBibTeXProtectCasing = true; bool Preferences::bibTeXProtectCasing() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXProtectCasing) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const bool valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXProtectCasing"), Preferences::defaultBibTeXProtectCasing); if (d->validateValueForBibTeXProtectCasing(valueFromConfig)) { d->cachedBibTeXProtectCasing = valueFromConfig; d->dirtyFlagBibTeXProtectCasing = false; } else { /// Configuration file setting for BibTeXProtectCasing has an invalid value, using default as fallback setBibTeXProtectCasing(Preferences::defaultBibTeXProtectCasing); } } return d->cachedBibTeXProtectCasing; #else // HAVE_KF5 return defaultBibTeXProtectCasing; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXProtectCasing(const bool newValue) { if (!d->validateValueForBibTeXProtectCasing(newValue)) return false; d->dirtyFlagBibTeXProtectCasing = false; d->cachedBibTeXProtectCasing = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const bool valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXProtectCasing"), Preferences::defaultBibTeXProtectCasing); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXProtectCasing"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QStringList Preferences::availableBibTeXListSeparators {QStringLiteral("; "), QStringLiteral(", ")}; const QString Preferences::defaultBibTeXListSeparator = Preferences::availableBibTeXListSeparators.front(); const QString &Preferences::bibTeXListSeparator() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXListSeparator) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXListSeparator"), Preferences::defaultBibTeXListSeparator); if (d->validateValueForBibTeXListSeparator(valueFromConfig)) { d->cachedBibTeXListSeparator = valueFromConfig; d->dirtyFlagBibTeXListSeparator = false; } else { /// Configuration file setting for BibTeXListSeparator has an invalid value, using default as fallback setBibTeXListSeparator(Preferences::defaultBibTeXListSeparator); } } return d->cachedBibTeXListSeparator; #else // HAVE_KF5 return defaultBibTeXListSeparator; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXListSeparator(const QString &newValue) { QString sanitizedNewValue = newValue; const QString lowerSanitizedNewValue = sanitizedNewValue.toLower(); for (const QString &knownBibTeXListSeparator : availableBibTeXListSeparators) if (knownBibTeXListSeparator.toLower() == lowerSanitizedNewValue) { sanitizedNewValue = knownBibTeXListSeparator; break; } if (!d->validateValueForBibTeXListSeparator(sanitizedNewValue)) return false; d->dirtyFlagBibTeXListSeparator = false; d->cachedBibTeXListSeparator = sanitizedNewValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterBibTeX")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXListSeparator"), Preferences::defaultBibTeXListSeparator); if (valueFromConfig == sanitizedNewValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXListSeparator"), sanitizedNewValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QString Preferences::defaultLaTeXBabelLanguage = QStringLiteral("english"); const QString &Preferences::laTeXBabelLanguage() { #ifdef HAVE_KF5 if (d->dirtyFlagLaTeXBabelLanguage) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterLaTeXbased")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("LaTeXBabelLanguage"), Preferences::defaultLaTeXBabelLanguage); if (d->validateValueForLaTeXBabelLanguage(valueFromConfig)) { d->cachedLaTeXBabelLanguage = valueFromConfig; d->dirtyFlagLaTeXBabelLanguage = false; } else { /// Configuration file setting for LaTeXBabelLanguage has an invalid value, using default as fallback setLaTeXBabelLanguage(Preferences::defaultLaTeXBabelLanguage); } } return d->cachedLaTeXBabelLanguage; #else // HAVE_KF5 return defaultLaTeXBabelLanguage; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setLaTeXBabelLanguage(const QString &newValue) { if (!d->validateValueForLaTeXBabelLanguage(newValue)) return false; d->dirtyFlagLaTeXBabelLanguage = false; d->cachedLaTeXBabelLanguage = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterLaTeXbased")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("LaTeXBabelLanguage"), Preferences::defaultLaTeXBabelLanguage); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("LaTeXBabelLanguage"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QString Preferences::defaultBibTeXBibliographyStyle = QStringLiteral("plain"); const QString &Preferences::bibTeXBibliographyStyle() { #ifdef HAVE_KF5 if (d->dirtyFlagBibTeXBibliographyStyle) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("FileExporterLaTeXbased")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXBibliographyStyle"), Preferences::defaultBibTeXBibliographyStyle); if (d->validateValueForBibTeXBibliographyStyle(valueFromConfig)) { d->cachedBibTeXBibliographyStyle = valueFromConfig; d->dirtyFlagBibTeXBibliographyStyle = false; } else { /// Configuration file setting for BibTeXBibliographyStyle has an invalid value, using default as fallback setBibTeXBibliographyStyle(Preferences::defaultBibTeXBibliographyStyle); } } return d->cachedBibTeXBibliographyStyle; #else // HAVE_KF5 return defaultBibTeXBibliographyStyle; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setBibTeXBibliographyStyle(const QString &newValue) { if (!d->validateValueForBibTeXBibliographyStyle(newValue)) return false; d->dirtyFlagBibTeXBibliographyStyle = false; d->cachedBibTeXBibliographyStyle = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("FileExporterLaTeXbased")); const QString valueFromConfig = configGroup.readEntry(QStringLiteral("BibTeXBibliographyStyle"), Preferences::defaultBibTeXBibliographyStyle); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("BibTeXBibliographyStyle"), newValue, KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 -const QVector> Preferences::availableFileViewDoubleClickActions {{Preferences::ActionOpenEditor, i18nc("What to do if double-clicking on a file view item", "Open Editor")}, {Preferences::ActionViewDocument, i18nc("What to do if double-clicking on a file view item", "View Document")}}; +const QVector> Preferences::availableFileViewDoubleClickActions {{Preferences::FileViewDoubleClickAction::OpenEditor, i18nc("What to do if double-clicking on a file view item", "Open Editor")}, {Preferences::FileViewDoubleClickAction::ViewDocument, i18nc("What to do if double-clicking on a file view item", "View Document")}}; const Preferences::FileViewDoubleClickAction Preferences::defaultFileViewDoubleClickAction = Preferences::availableFileViewDoubleClickActions.front().first; Preferences::FileViewDoubleClickAction Preferences::fileViewDoubleClickAction() { #ifdef HAVE_KF5 if (d->dirtyFlagFileViewDoubleClickAction) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("User Interface")); const Preferences::FileViewDoubleClickAction valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("FileViewDoubleClickAction"), static_cast(Preferences::defaultFileViewDoubleClickAction))); if (d->validateValueForFileViewDoubleClickAction(valueFromConfig)) { d->cachedFileViewDoubleClickAction = valueFromConfig; d->dirtyFlagFileViewDoubleClickAction = false; } else { /// Configuration file setting for FileViewDoubleClickAction has an invalid value, using default as fallback setFileViewDoubleClickAction(Preferences::defaultFileViewDoubleClickAction); } } return d->cachedFileViewDoubleClickAction; #else // HAVE_KF5 return defaultFileViewDoubleClickAction; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setFileViewDoubleClickAction(const Preferences::FileViewDoubleClickAction newValue) { if (!d->validateValueForFileViewDoubleClickAction(newValue)) return false; d->dirtyFlagFileViewDoubleClickAction = false; d->cachedFileViewDoubleClickAction = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("User Interface")); const Preferences::FileViewDoubleClickAction valueFromConfig = static_cast(configGroup.readEntry(QStringLiteral("FileViewDoubleClickAction"), static_cast(Preferences::defaultFileViewDoubleClickAction))); if (valueFromConfig == newValue) return false; configGroup.writeEntry(QStringLiteral("FileViewDoubleClickAction"), static_cast(newValue), KConfig::Notify); d->config->sync(); return true; } #endif // HAVE_KF5 const QVector> Preferences::defaultColorCodes {{QColor(0xcc, 0x33, 0x00), i18nc("Color Labels", "Important")}, {QColor(0x00, 0x33, 0xff), i18nc("Color Labels", "Unread")}, {QColor(0x00, 0x99, 0x66), i18nc("Color Labels", "Read")}, {QColor(0xf0, 0xd0, 0x00), i18nc("Color Labels", "Watch")}}; const QVector> &Preferences::colorCodes() { #ifdef HAVE_KF5 if (d->dirtyFlagColorCodes) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("Color Labels")); const QVector> valueFromConfig = d->readEntryColorCodes(configGroup, QStringLiteral("ColorCodes")); if (d->validateValueForColorCodes(valueFromConfig)) { d->cachedColorCodes = valueFromConfig; d->dirtyFlagColorCodes = false; } else { /// Configuration file setting for ColorCodes has an invalid value, using default as fallback setColorCodes(Preferences::defaultColorCodes); } } return d->cachedColorCodes; #else // HAVE_KF5 return defaultColorCodes; #endif // HAVE_KF5 } #ifdef HAVE_KF5 bool Preferences::setColorCodes(const QVector> &newValue) { if (!d->validateValueForColorCodes(newValue)) return false; d->dirtyFlagColorCodes = false; d->cachedColorCodes = newValue; static KConfigGroup configGroup(d->config, QStringLiteral("Color Labels")); const QVector> valueFromConfig = d->readEntryColorCodes(configGroup, QStringLiteral("ColorCodes")); if (valueFromConfig == newValue) return false; d->writeEntryColorCodes(configGroup, QStringLiteral("ColorCodes"), newValue); d->config->sync(); return true; } #endif // HAVE_KF5 diff --git a/src/config/preferences.h b/src/config/preferences.h index def42e4a..468c7ab1 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -1,307 +1,307 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 . * ***************************************************************************/ /// This file has been automatically generated using the script 'preferences-generator.py' /// based on configuration data from file 'preferences.json'. If there are any problems or /// bugs, you need to fix those two files and re-generated both 'preferences.h' and /// 'preferences.cpp'. Manual changes in this file will be overwritten the next time the /// script will be run. You have been warned. #ifndef KBIBTEX_CONFIG_PREFERENCES_H #define KBIBTEX_CONFIG_PREFERENCES_H #include #include #include #ifdef HAVE_KF5 #include "kbibtexconfig_export.h" #endif // HAVE_KF5 class KBIBTEXCONFIG_EXPORT Preferences { public: static Preferences &instance(); ~Preferences(); - enum BackupScope { NoBackup, LocalOnly, BothLocalAndRemote }; - enum BibliographySystem { BibTeX = 0, BibLaTeX = 1 }; - enum FileViewDoubleClickAction { ActionOpenEditor = 0, ActionViewDocument = 1 }; - enum QuoteComment { qcNone = 0, qcCommand = 1, qcPercentSign = 2 }; + enum class BackupScope { None, LocalOnly, BothLocalAndRemote }; + enum class BibliographySystem { BibTeX, BibLaTeX }; + enum class FileViewDoubleClickAction { OpenEditor, ViewDocument }; + enum class QuoteComment { None, Command, PercentSign }; /// *** BibliographySystem of type BibliographySystem *** static const BibliographySystem defaultBibliographySystem; static const QVector> availableBibliographySystems; BibliographySystem bibliographySystem(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibliographySystem(const BibliographySystem bibliographySystem); #endif // HAVE_KF5 /// *** PersonNameFormat of type QString *** static const QString personNameFormatLastFirst; static const QString personNameFormatFirstLast; static const QString defaultPersonNameFormat; const QString &personNameFormat(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setPersonNameFormat(const QString &personNameFormat); #endif // HAVE_KF5 /// *** CopyReferenceCommand of type QString *** static const QString defaultCopyReferenceCommand; static const QStringList availableCopyReferenceCommands; const QString ©ReferenceCommand(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setCopyReferenceCommand(const QString ©ReferenceCommand); #endif // HAVE_KF5 /// *** PageSize of type QPageSize::PageSizeId *** static const QPageSize::PageSizeId defaultPageSize; static const QVector> availablePageSizes; QPageSize::PageSizeId pageSize(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setPageSize(const QPageSize::PageSizeId pageSize); #endif // HAVE_KF5 /// *** BackupScope of type BackupScope *** static const BackupScope defaultBackupScope; static const QVector> availableBackupScopes; BackupScope backupScope(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBackupScope(const BackupScope backupScope); #endif // HAVE_KF5 /// *** NumberOfBackups of type int *** static const int defaultNumberOfBackups; int numberOfBackups(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setNumberOfBackups(const int numberOfBackups); #endif // HAVE_KF5 /// *** IdSuggestionFormatStrings of type QStringList *** static const QStringList defaultIdSuggestionFormatStrings; const QStringList &idSuggestionFormatStrings(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setIdSuggestionFormatStrings(const QStringList &idSuggestionFormatStrings); #endif // HAVE_KF5 /// *** ActiveIdSuggestionFormatString of type QString *** static const QString defaultActiveIdSuggestionFormatString; const QString &activeIdSuggestionFormatString(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setActiveIdSuggestionFormatString(const QString &activeIdSuggestionFormatString); #endif // HAVE_KF5 /// *** LyXUseAutomaticPipeDetection of type bool *** static const bool defaultLyXUseAutomaticPipeDetection; bool lyXUseAutomaticPipeDetection(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setLyXUseAutomaticPipeDetection(const bool lyXUseAutomaticPipeDetection); #endif // HAVE_KF5 /// *** LyXPipePath of type QString *** static const QString defaultLyXPipePath; const QString &lyXPipePath(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setLyXPipePath(const QString &lyXPipePath); #endif // HAVE_KF5 /// *** BibTeXEncoding of type QString *** static const QString defaultBibTeXEncoding; static const QStringList availableBibTeXEncodings; const QString &bibTeXEncoding(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXEncoding(const QString &bibTeXEncoding); #endif // HAVE_KF5 /// *** BibTeXStringDelimiter of type QString *** static const QString defaultBibTeXStringDelimiter; static const QStringList availableBibTeXStringDelimiters; const QString &bibTeXStringDelimiter(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXStringDelimiter(const QString &bibTeXStringDelimiter); #endif // HAVE_KF5 /// *** BibTeXQuoteComment of type QuoteComment *** static const QuoteComment defaultBibTeXQuoteComment; static const QVector> availableBibTeXQuoteComments; QuoteComment bibTeXQuoteComment(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXQuoteComment(const QuoteComment bibTeXQuoteComment); #endif // HAVE_KF5 /// *** BibTeXKeywordCasing of type KBibTeX::Casing *** static const KBibTeX::Casing defaultBibTeXKeywordCasing; static const QVector> availableBibTeXKeywordCasings; KBibTeX::Casing bibTeXKeywordCasing(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXKeywordCasing(const KBibTeX::Casing bibTeXKeywordCasing); #endif // HAVE_KF5 /// *** BibTeXProtectCasing of type bool *** static const bool defaultBibTeXProtectCasing; bool bibTeXProtectCasing(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXProtectCasing(const bool bibTeXProtectCasing); #endif // HAVE_KF5 /// *** BibTeXListSeparator of type QString *** static const QString defaultBibTeXListSeparator; static const QStringList availableBibTeXListSeparators; const QString &bibTeXListSeparator(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXListSeparator(const QString &bibTeXListSeparator); #endif // HAVE_KF5 /// *** LaTeXBabelLanguage of type QString *** static const QString defaultLaTeXBabelLanguage; const QString &laTeXBabelLanguage(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setLaTeXBabelLanguage(const QString &laTeXBabelLanguage); #endif // HAVE_KF5 /// *** BibTeXBibliographyStyle of type QString *** static const QString defaultBibTeXBibliographyStyle; const QString &bibTeXBibliographyStyle(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setBibTeXBibliographyStyle(const QString &bibTeXBibliographyStyle); #endif // HAVE_KF5 /// *** FileViewDoubleClickAction of type FileViewDoubleClickAction *** static const FileViewDoubleClickAction defaultFileViewDoubleClickAction; static const QVector> availableFileViewDoubleClickActions; FileViewDoubleClickAction fileViewDoubleClickAction(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setFileViewDoubleClickAction(const FileViewDoubleClickAction fileViewDoubleClickAction); #endif // HAVE_KF5 /// *** ColorCodes of type QVector> *** static const QVector> defaultColorCodes; const QVector> &colorCodes(); #ifdef HAVE_KF5 /*! * @return true if this setting has been changed, i.e. the new value was different from the old value; false otherwise or under error conditions */ bool setColorCodes(const QVector> &colorCodes); #endif // HAVE_KF5 private: Q_DISABLE_COPY(Preferences) explicit Preferences(); class Private; Private *const d; }; #endif // KBIBTEX_CONFIG_PREFERENCES_H diff --git a/src/config/preferences.json b/src/config/preferences.json index c032afd6..6b3aca63 100644 --- a/src/config/preferences.json +++ b/src/config/preferences.json @@ -1,204 +1,204 @@ { "headerincludes": [ "" ], "implementationincludes": [ "", "" ], "enums": { "BibliographySystem": [ - ["BibTeX", 0], - ["BibLaTeX", 1] + "BibTeX", + "BibLaTeX" ], "BackupScope": [ - "NoBackup", + "None", "LocalOnly", "BothLocalAndRemote" ], "QuoteComment": [ - ["qcNone", 0], - ["qcCommand", 1], - ["qcPercentSign", 2] + "None", + "Command", + "PercentSign" ], "FileViewDoubleClickAction": [ - ["ActionOpenEditor", 0], - ["ActionViewDocument", 1] + "OpenEditor", + "ViewDocument" ] }, "settings": [ { "stem": "BibliographySystem", "type": "BibliographySystem", "availabletype": "QVector>", - "available": "{{Preferences::BibTeX, i18n(\"BibTeX\")}, {Preferences::BibLaTeX, i18n(\"BibLaTeX\")}}", + "available": "{{Preferences::BibliographySystem::BibTeX, i18n(\"BibTeX\")}, {Preferences::BibliographySystem::BibLaTeX, i18n(\"BibLaTeX\")}}", "notificationevent": "NotificationHub::EventBibliographySystemChanged" }, { "stem": "PersonNameFormat", "type": "QString", "default": "LastFirst", "predefined": [ ["LastFirst", "QStringLiteral(\"<%l><, %s><, %f>\")"], ["FirstLast", "QStringLiteral(\"<%f ><%l>< %s>\")"] ], "validationcode": "return valueToBeChecked.contains(QStringLiteral(\"%f\")) && valueToBeChecked.contains(QStringLiteral(\"%l\")) && valueToBeChecked.contains(QStringLiteral(\"%s\"));", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "CopyReferenceCommand", "type": "QString", "configgroup": "LaTeX", "availabletype": "QStringList", "available": "{QStringLiteral(\"cite\"), QStringLiteral(\"citealt\"), QStringLiteral(\"citeauthor\"), QStringLiteral(\"citeauthor*\"), QStringLiteral(\"citeyear\"), QStringLiteral(\"citeyearpar\"), QStringLiteral(\"shortcite\"), QStringLiteral(\"citet\"), QStringLiteral(\"citet*\"), QStringLiteral(\"citep\"), QStringLiteral(\"citep*\")}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "headerincludes": [""], "stem": "PageSize", "type": "QPageSize::PageSizeId", "podtype": "int", "availabletype": "QVector>", "available": "{{QPageSize::A4, QStringLiteral(\"a4paper\")}, {QPageSize::Letter, QStringLiteral(\"letter\")}, {QPageSize::Legal, QStringLiteral(\"legal\")}}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BackupScope", "type": "BackupScope", "configgroup": "InputOutput", "availabletype": "QVector>", - "available": "{{Preferences::NoBackup, i18n(\"No backups\")}, {Preferences::LocalOnly, i18n(\"Local files only\")}, {Preferences::BothLocalAndRemote, i18n(\"Both local and remote files\")}}", + "available": "{{Preferences::BackupScope::None, i18n(\"No backups\")}, {Preferences::BackupScope::LocalOnly, i18n(\"Local files only\")}, {Preferences::BackupScope::BothLocalAndRemote, i18n(\"Both local and remote files\")}}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "NumberOfBackups", "type": "int", "configgroup": "InputOutput", "default": "5", "validationcode": "return valueToBeChecked >= 0;", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "IdSuggestionFormatStrings", "type": "QStringList", "configgroup": "IdSuggestions", "default": "{QStringLiteral(\"A\"), QStringLiteral(\"A2|y\"), QStringLiteral(\"A3|y\"), QStringLiteral(\"A4|y|\\\":|T5\"), QStringLiteral(\"al|\\\":|T\"), QStringLiteral(\"al|y\"), QStringLiteral(\"al|Y\"), QStringLiteral(\"Al\\\"-|\\\"-|y\"), QStringLiteral(\"Al\\\"+|Y\"), QStringLiteral(\"al|y|T\"), QStringLiteral(\"al|Y|T3\"), QStringLiteral(\"al|Y|T3l\"), QStringLiteral(\"a|\\\":|Y|\\\":|T1\"), QStringLiteral(\"a|y\"), QStringLiteral(\"A|\\\":|Y\")}", "validationcode": "return !valueToBeChecked.isEmpty() && (valueToBeChecked.front().contains(QLatin1Char('A')) || valueToBeChecked.front().contains(QLatin1Char('a')) || valueToBeChecked.front().contains(QLatin1Char('y')) || valueToBeChecked.front().contains(QLatin1Char('Y')) || valueToBeChecked.front().contains(QLatin1Char('T')));", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "ActiveIdSuggestionFormatString", "type": "QString", "configgroup": "IdSuggestions", "default": "{}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "LyXUseAutomaticPipeDetection", "type": "bool", "configgroup": "LyX", "default": "true", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "LyXPipePath", "type": "QString", "configgroup": "LyX", "default": "QDir::homePath() + QStringLiteral(\"/.lyxpipe.in\")", "validationcode": "return valueToBeChecked.startsWith(QLatin1Char('/'));", "sanitizecode": [ "QString sanitizedNewValue = newValue;", "if (sanitizedNewValue.endsWith(QStringLiteral(\".out\")))", " sanitizedNewValue = sanitizedNewValue.left(sanitizedNewValue.length() - 4);", "if (!sanitizedNewValue.endsWith(QStringLiteral(\".in\")))", " sanitizedNewValue = sanitizedNewValue.append(QStringLiteral(\".in\"));" ], "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXEncoding", "type": "QString", "configgroup": "FileExporterBibTeX", "default": "QStringLiteral(\"UTF-8\")", "availabletype": "QStringList", "available": "{QStringLiteral(\"LaTeX\"), QStringLiteral(\"US-ASCII\"), QStringLiteral(\"ISO-8859-1\"), QStringLiteral(\"ISO-8859-2\"), QStringLiteral(\"ISO-8859-3\"), QStringLiteral(\"ISO-8859-4\"), QStringLiteral(\"ISO-8859-5\"), QStringLiteral(\"ISO-8859-6\"), QStringLiteral(\"ISO-8859-7\"), QStringLiteral(\"ISO-8859-8\"), QStringLiteral(\"ISO-8859-9\"), QStringLiteral(\"ISO-8859-10\"), QStringLiteral(\"ISO-8859-13\"), QStringLiteral(\"ISO-8859-14\"), QStringLiteral(\"ISO-8859-15\"), QStringLiteral(\"ISO-8859-16\"), QStringLiteral(\"UTF-8\"), QStringLiteral(\"UTF-16\"), QStringLiteral(\"UTF-16BE\"), QStringLiteral(\"UTF-16LE\"), QStringLiteral(\"UTF-32\"), QStringLiteral(\"UTF-32BE\"), QStringLiteral(\"UTF-32LE\"), QStringLiteral(\"KOI8-R\"), QStringLiteral(\"KOI8-U\"), QStringLiteral(\"Big5\"), QStringLiteral(\"Big5-HKSCS\"), QStringLiteral(\"GB18030\"), QStringLiteral(\"EUC-JP\"), QStringLiteral(\"EUC-KR\"), QStringLiteral(\"ISO 2022-JP\"), QStringLiteral(\"Shift-JIS\"), QStringLiteral(\"Windows-1250\"), QStringLiteral(\"Windows-1251\"), QStringLiteral(\"Windows-1252\"), QStringLiteral(\"Windows-1253\"), QStringLiteral(\"Windows-1254\"), QStringLiteral(\"Windows-1255\"), QStringLiteral(\"Windows-1256\"), QStringLiteral(\"Windows-1257\"), QStringLiteral(\"Windows-1258\")}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXStringDelimiter", "type": "QString", "configgroup": "FileExporterBibTeX", "availabletype": "QStringList", "available": "{QStringLiteral(\"{}\"), QStringLiteral(\"\\\"\\\"\"), QStringLiteral(\"()\")}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXQuoteComment", "type": "QuoteComment", "configgroup": "FileExporterBibTeX", "availabletype": "QVector>", - "available": "{{Preferences::qcNone, i18nc(\"Comment Quoting\", \"None\")}, {Preferences::qcCommand, i18nc(\"Comment Quoting\", \"@comment{\\342\\200\\246}\")}, {Preferences::qcPercentSign, i18nc(\"Comment Quoting\", \"% \\342\\200\\246\")}}", + "available": "{{Preferences::QuoteComment::None, i18nc(\"Comment Quoting\", \"None\")}, {Preferences::QuoteComment::Command, i18nc(\"Comment Quoting\", \"@comment{\\342\\200\\246}\")}, {Preferences::QuoteComment::PercentSign, i18nc(\"Comment Quoting\", \"% \\342\\200\\246\")}}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXKeywordCasing", "type": "KBibTeX::Casing", "podtype": "int", "configgroup": "FileExporterBibTeX", "availabletype": "QVector>", - "available": "{{KBibTeX::cLowerCase, i18nc(\"Casing of strings\", \"lowercase\")}, {KBibTeX::cInitialCapital, i18nc(\"Casing of strings\", \"Initial capital\")}, {KBibTeX::cUpperCamelCase, i18nc(\"Casing of strings\", \"UpperCamelCase\")}, {KBibTeX::cLowerCamelCase, i18nc(\"Casing of strings\", \"lowerCamelCase\")}, {KBibTeX::cUpperCase, i18nc(\"Casing of strings\", \"UPPERCASE\")}}", + "available": "{{KBibTeX::Casing::LowerCase, i18nc(\"Casing of strings\", \"lowercase\")}, {KBibTeX::Casing::InitialCapital, i18nc(\"Casing of strings\", \"Initial capital\")}, {KBibTeX::Casing::UpperCamelCase, i18nc(\"Casing of strings\", \"UpperCamelCase\")}, {KBibTeX::Casing::LowerCamelCase, i18nc(\"Casing of strings\", \"lowerCamelCase\")}, {KBibTeX::Casing::UpperCase, i18nc(\"Casing of strings\", \"UPPERCASE\")}}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXProtectCasing", "type": "bool", "default": "true", "configgroup": "FileExporterBibTeX", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXListSeparator", "type": "QString", "configgroup": "FileExporterBibTeX", "availabletype": "QStringList", "available": "{QStringLiteral(\"; \"), QStringLiteral(\", \")}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "LaTeXBabelLanguage", "type": "QString", "configgroup": "FileExporterLaTeXbased", "default": "QStringLiteral(\"english\")", "validationcode": "return !valueToBeChecked.isEmpty();", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "BibTeXBibliographyStyle", "type": "QString", "configgroup": "FileExporterLaTeXbased", "default": "QStringLiteral(\"plain\")", "validationcode": "return !valueToBeChecked.isEmpty();", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "stem": "FileViewDoubleClickAction", "type": "FileViewDoubleClickAction", "podtype": "int", "configgroup": "User Interface", "availabletype": "QVector>", - "available": "{{Preferences::ActionOpenEditor, i18nc(\"What to do if double-clicking on a file view item\", \"Open Editor\")}, {Preferences::ActionViewDocument, i18nc(\"What to do if double-clicking on a file view item\", \"View Document\")}}", + "available": "{{Preferences::FileViewDoubleClickAction::OpenEditor, i18nc(\"What to do if double-clicking on a file view item\", \"Open Editor\")}, {Preferences::FileViewDoubleClickAction::ViewDocument, i18nc(\"What to do if double-clicking on a file view item\", \"View Document\")}}", "notificationevent": "NotificationHub::EventConfigurationChanged" }, { "headerincludes": [""], "stem": "ColorCodes", "type": "QVector>", "configgroup": "Color Labels", "default": "{{QColor(0xcc, 0x33, 0x00), i18nc(\"Color Labels\", \"Important\")}, {QColor(0x00, 0x33, 0xff), i18nc(\"Color Labels\", \"Unread\")}, {QColor(0x00, 0x99, 0x66), i18nc(\"Color Labels\", \"Read\")}, {QColor(0xf0, 0xd0, 0x00), i18nc(\"Color Labels\", \"Watch\")}}", "validationcode": ["static const QColor white(Qt::white), black(Qt::black);", "for (QVector>::ConstIterator it = valueToBeChecked.constBegin(); it != valueToBeChecked.constEnd(); ++it)", " if (it->first == white || it->first == black || it->second.isEmpty())", " return false;", "return true;"], "notificationevent": "NotificationHub::EventConfigurationChanged", "readEntry": ["const QString rawEntry = configGroup.readEntry(key, QString());", "if (rawEntry.isEmpty()) return Preferences::defaultColorCodes;", "const QStringList pairs = rawEntry.split(QStringLiteral(\"\\0\\0\"), QString::SkipEmptyParts);", "if (pairs.isEmpty()) return Preferences::defaultColorCodes;", "QVector> result;", "for (const QString &pair : pairs) {", " const QStringList colorLabelPair = pair.split(QStringLiteral(\"\\0\"), QString::SkipEmptyParts);", " if (colorLabelPair.length() != 2) return Preferences::defaultColorCodes;", " result.append(qMakePair(QColor(colorLabelPair[0]), colorLabelPair[1]));", "}", "return result;"], "writeEntry": ["QString rawEntry;", "for (QVector>::ConstIterator it = valueToBeWritten.constBegin(); it != valueToBeWritten.constEnd(); ++it) {", " if (!rawEntry.isEmpty()) rawEntry.append(QStringLiteral(\"\\0\\0\"));", " rawEntry = rawEntry.append(it->first.name(QColor::HexRgb)).append(QStringLiteral(\"\\0\")).append(it->second);", "}", "configGroup.writeEntry(key, rawEntry, KConfig::Notify);"] } ] } diff --git a/src/data/entry.cpp b/src/data/entry.cpp index be557507..2f6fb407 100644 --- a/src/data/entry.cpp +++ b/src/data/entry.cpp @@ -1,275 +1,275 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 "entry.h" #include #include "file.h" #include "logging_data.h" // FIXME: Check if using those constants in the program is really necessary // or can be replace by config files const QString Entry::ftAbstract = QStringLiteral("abstract"); const QString Entry::ftAddress = QStringLiteral("address"); const QString Entry::ftAuthor = QStringLiteral("author"); const QString Entry::ftBookTitle = QStringLiteral("booktitle"); const QString Entry::ftChapter = QStringLiteral("chapter"); const QString Entry::ftColor = QStringLiteral("x-color"); const QString Entry::ftComment = QStringLiteral("comment"); const QString Entry::ftCrossRef = QStringLiteral("crossref"); const QString Entry::ftDOI = QStringLiteral("doi"); const QString Entry::ftEditor = QStringLiteral("editor"); const QString Entry::ftFile = QStringLiteral("file"); const QString Entry::ftISSN = QStringLiteral("issn"); const QString Entry::ftISBN = QStringLiteral("isbn"); const QString Entry::ftJournal = QStringLiteral("journal"); const QString Entry::ftKeywords = QStringLiteral("keywords"); const QString Entry::ftLocalFile = QStringLiteral("localfile"); const QString Entry::ftLocation = QStringLiteral("location"); const QString Entry::ftMonth = QStringLiteral("month"); const QString Entry::ftNote = QStringLiteral("note"); const QString Entry::ftNumber = QStringLiteral("number"); const QString Entry::ftPages = QStringLiteral("pages"); const QString Entry::ftPublisher = QStringLiteral("publisher"); const QString Entry::ftSchool = QStringLiteral("school"); const QString Entry::ftSeries = QStringLiteral("series"); const QString Entry::ftStarRating = QStringLiteral("x-stars"); const QString Entry::ftTitle = QStringLiteral("title"); const QString Entry::ftUrl = QStringLiteral("url"); const QString Entry::ftUrlDate = QStringLiteral("urldate"); const QString Entry::ftVolume = QStringLiteral("volume"); const QString Entry::ftYear = QStringLiteral("year"); const QString Entry::ftXData = QStringLiteral("xdata"); const QString Entry::etArticle = QStringLiteral("article"); const QString Entry::etBook = QStringLiteral("book"); const QString Entry::etInBook = QStringLiteral("inbook"); const QString Entry::etInProceedings = QStringLiteral("inproceedings"); const QString Entry::etProceedings = QStringLiteral("proceedings"); const QString Entry::etMisc = QStringLiteral("misc"); const QString Entry::etPhDThesis = QStringLiteral("phdthesis"); const QString Entry::etMastersThesis = QStringLiteral("mastersthesis"); const QString Entry::etTechReport = QStringLiteral("techreport"); const QString Entry::etUnpublished = QStringLiteral("unpublished"); quint64 Entry::internalUniqueIdCounter = 0; /** * Private class to store internal variables that should not be visible * in the interface as defined in the header file. */ class Entry::EntryPrivate { public: QString type; QString id; }; Entry::Entry(const QString &type, const QString &id) : Element(), QMap(), internalUniqueId(++internalUniqueIdCounter), d(new Entry::EntryPrivate) { d->type = type; d->id = id; } Entry::Entry(const Entry &other) : Element(), QMap(), internalUniqueId(++internalUniqueIdCounter), d(new Entry::EntryPrivate) { operator=(other); } Entry::~Entry() { clear(); delete d; } quint64 Entry::uniqueId() const { return internalUniqueId; } bool Entry::operator==(const Entry &other) const { /// Quick and easy tests first: id, type, and numer of fields if (id() != other.id() || type().compare(other.type(), Qt::CaseInsensitive) != 0 || count() != other.count()) return false; /// Compare each field with other's corresponding field for (Entry::ConstIterator it = constBegin(); it != constEnd(); ++it) { if (!other.contains(it.key())) return false; const Value &thisValue = it.value(); const Value &otherValue = other.value(it.key()); if (thisValue != otherValue) return false; } /// All fields of the other entry must occurr as well in this entry /// (no need to check equivalence again) for (Entry::ConstIterator it = other.constBegin(); it != other.constEnd(); ++it) if (!contains(it.key())) return false; return true; } bool Entry::operator!=(const Entry &other) const { return !operator ==(other); } Entry &Entry::operator= (const Entry &other) { if (this != &other) { d->type = other.type(); d->id = other.id(); clear(); for (Entry::ConstIterator it = other.constBegin(); it != other.constEnd(); ++it) insert(it.key(), it.value()); } return *this; } void Entry::setType(const QString &type) { d->type = type; } QString Entry::type() const { return d->type; } void Entry::setId(const QString &id) { d->id = id; } QString Entry::id() const { return d->id; } const Value Entry::value(const QString &key) const { const QString lcKey = key.toLower(); for (Entry::ConstIterator it = constBegin(); it != constEnd(); ++it) if (it.key().toLower() == lcKey) return QMap::value(it.key()); return Value(); } int Entry::remove(const QString &key) { const QString lcKey = key.toLower(); for (Entry::Iterator it = begin(); it != end(); ++it) if (it.key().toLower() == lcKey) { QMap::erase(it); return 1; } return 0; } bool Entry::contains(const QString &key) const { const QString lcKey = key.toLower(); for (Entry::ConstIterator it = constBegin(); it != constEnd(); ++it) if (it.key().toLower() == lcKey) return true; return false; } Entry *Entry::resolveCrossref(const File *bibTeXfile) const { Entry *result = new Entry(*this); if (bibTeXfile == nullptr) return result; static const QStringList crossRefFields = {ftCrossRef, ftXData}; for (const QString &crossRefField : crossRefFields) { const QString crossRefValue = PlainTextValue::text(result->value(crossRefField)); if (crossRefValue.isEmpty()) continue; - const QSharedPointer crossRefEntry = bibTeXfile->containsKey(crossRefField, File::etEntry).dynamicCast(); + const QSharedPointer crossRefEntry = bibTeXfile->containsKey(crossRefField, File::ElementType::Entry).dynamicCast(); if (!crossRefEntry.isNull()) { /// Copy all fields from crossref'ed entry to new entry which do not (yet) exist in the new entry for (Entry::ConstIterator it = crossRefEntry->constBegin(); it != crossRefEntry->constEnd(); ++it) if (!result->contains(it.key())) result->insert(it.key(), Value(it.value())); if (crossRefEntry->type().compare(etProceedings, Qt::CaseInsensitive) && result->type().compare(etInProceedings, Qt::CaseInsensitive) && crossRefEntry->contains(ftTitle) && !result->contains(ftBookTitle)) { /// In case current entry is of type 'inproceedings' but lacks a 'book title' /// and the crossref'ed entry is of type 'proceedings' and has a 'title', then /// copy this 'title into as the 'book title' of the current entry. /// Note: the correct way should be that the crossref'ed entry has a 'book title' /// field, but that case was handled above when copying non-existing fields, /// so this if-block is only a fall-back case. result->insert(ftBookTitle, Value(crossRefEntry->operator [](ftTitle))); } /// Remove crossref field (no longer of use as all data got copied) result->remove(crossRefField); } } return result; } QStringList Entry::authorsLastName(const Entry &entry) { Value value; if (entry.contains(Entry::ftAuthor)) value = entry.value(Entry::ftAuthor); if (value.isEmpty() && entry.contains(Entry::ftEditor)) value = entry.value(Entry::ftEditor); if (value.isEmpty()) return QStringList(); QStringList result; int maxAuthors = 16; ///< limit the number of authors considered result.reserve(maxAuthors); for (const QSharedPointer &item : const_cast(value)) { QSharedPointer person = item.dynamicCast(); if (!person.isNull()) { const QString lastName = person->lastName(); if (!lastName.isEmpty()) result << lastName; } if (--maxAuthors <= 0) break; ///< limit the number of authors considered } return result; } QStringList Entry::authorsLastName() const { return authorsLastName(*this); } bool Entry::isEntry(const Element &other) { return typeid(other) == typeid(Entry); } QDebug operator<<(QDebug dbg, const Entry &entry) { dbg.nospace() << "Entry " << entry.id() << " (uniqueId=" << entry.uniqueId() << "), has " << entry.count() << " key-value pairs"; return dbg; } diff --git a/src/data/file.cpp b/src/data/file.cpp index 892d7036..a65d9c3d 100644 --- a/src/data/file.cpp +++ b/src/data/file.cpp @@ -1,338 +1,338 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 "file.h" #include #include #include #include #include #include "entry.h" #include "element.h" #include "macro.h" #include "comment.h" #include "preamble.h" #include "logging_data.h" const QString File::Url = QStringLiteral("Url"); const QString File::Encoding = QStringLiteral("Encoding"); const QString File::StringDelimiter = QStringLiteral("StringDelimiter"); const QString File::QuoteComment = QStringLiteral("QuoteComment"); const QString File::KeywordCasing = QStringLiteral("KeywordCasing"); const QString File::ProtectCasing = QStringLiteral("ProtectCasing"); const QString File::NameFormatting = QStringLiteral("NameFormatting"); const QString File::ListSeparator = QStringLiteral("ListSeparator"); const quint64 valid = 0x08090a0b0c0d0e0f; const quint64 invalid = 0x0102030405060708; class File::FilePrivate { private: quint64 validInvalidField; static const quint64 initialInternalIdCounter; static quint64 internalIdCounter; public: const quint64 internalId; QHash properties; explicit FilePrivate(File *parent) : validInvalidField(valid), internalId(++internalIdCounter) { Q_UNUSED(parent) const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Creating File instance" << internalId << " Valid?" << isValid; loadConfiguration(); } ~FilePrivate() { const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Deleting File instance" << internalId << " Valid?" << isValid; validInvalidField = invalid; } /// Copy-assignment operator FilePrivate &operator= (const FilePrivate &other) { if (this != &other) { validInvalidField = other.validInvalidField; properties = other.properties; const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << " Is other valid?" << other.checkValidity() << " Self valid?" << isValid; } return *this; } /// Move-assignment operator FilePrivate &operator= (FilePrivate &&other) { if (this != &other) { validInvalidField = std::move(other.validInvalidField); properties = std::move(other.properties); const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << " Is other valid?" << other.checkValidity() << " Self valid?" << isValid; } return *this; } void loadConfiguration() { /// Load and set configuration as stored in settings properties.insert(File::Encoding, Preferences::instance().bibTeXEncoding()); properties.insert(File::StringDelimiter, Preferences::instance().bibTeXStringDelimiter()); - properties.insert(File::QuoteComment, Preferences::instance().bibTeXQuoteComment()); - properties.insert(File::KeywordCasing, Preferences::instance().bibTeXKeywordCasing()); + properties.insert(File::QuoteComment, static_cast(Preferences::instance().bibTeXQuoteComment())); + properties.insert(File::KeywordCasing, static_cast(Preferences::instance().bibTeXKeywordCasing())); properties.insert(File::NameFormatting, Preferences::instance().personNameFormat()); properties.insert(File::ProtectCasing, static_cast(Preferences::instance().bibTeXProtectCasing() ? Qt::Checked : Qt::Unchecked)); properties.insert(File::ListSeparator, Preferences::instance().bibTeXListSeparator()); } bool checkValidity() const { if (validInvalidField != valid) { /// 'validInvalidField' must equal to the know 'valid' value qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << validInvalidField << "!=" << valid; return false; } else if (internalId <= initialInternalIdCounter) { /// Internal id counter starts at initialInternalIdCounter+1 qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "< " << (initialInternalIdCounter + 1); return false; } else if (internalId > 600000) { /// Reasonable assumption: not more that 500000 ids used qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "> 600000"; return false; } return true; } }; const quint64 File::FilePrivate::initialInternalIdCounter = 99999; quint64 File::FilePrivate::internalIdCounter = File::FilePrivate::initialInternalIdCounter; File::File() : QList >(), d(new FilePrivate(this)) { /// nothing } File::File(const File &other) : QList >(other), d(new FilePrivate(this)) { d->operator =(*other.d); } File::File(File &&other) : QList >(std::move(other)), d(new FilePrivate(this)) { d->operator =(std::move(*other.d)); } File::~File() { Q_ASSERT_X(d->checkValidity(), "File::~File()", "This File object is not valid"); delete d; } File &File::operator= (const File &other) { if (this != &other) d->operator =(*other.d); return *this; } File &File::operator= (File &&other) { if (this != &other) d->operator =(std::move(*other.d)); return *this; } bool File::operator==(const File &other) const { if (size() != other.size()) return false; for (File::ConstIterator myIt = constBegin(), otherIt = other.constBegin(); myIt != constEnd() && otherIt != constEnd(); ++myIt, ++otherIt) { QSharedPointer myEntry = myIt->dynamicCast(); QSharedPointer otherEntry = otherIt->dynamicCast(); if ((myEntry.isNull() && !otherEntry.isNull()) || (!myEntry.isNull() && otherEntry.isNull())) return false; if (!myEntry.isNull() && !otherEntry.isNull()) { if (myEntry->operator !=(*otherEntry.data())) return false; } else { QSharedPointer myMacro = myIt->dynamicCast(); QSharedPointer otherMacro = otherIt->dynamicCast(); if ((myMacro.isNull() && !otherMacro.isNull()) || (!myMacro.isNull() && otherMacro.isNull())) return false; if (!myMacro.isNull() && !otherMacro.isNull()) { if (myMacro->operator !=(*otherMacro.data())) return false; } else { QSharedPointer myPreamble = myIt->dynamicCast(); QSharedPointer otherPreamble = otherIt->dynamicCast(); if ((myPreamble.isNull() && !otherPreamble.isNull()) || (!myPreamble.isNull() && otherPreamble.isNull())) return false; if (!myPreamble.isNull() && !otherPreamble.isNull()) { if (myPreamble->operator !=(*otherPreamble.data())) return false; } else { QSharedPointer myComment = myIt->dynamicCast(); QSharedPointer otherComment = otherIt->dynamicCast(); if ((myComment.isNull() && !otherComment.isNull()) || (!myComment.isNull() && otherComment.isNull())) return false; if (!myComment.isNull() && !otherComment.isNull()) { // TODO right now, don't care if comments are equal qCDebug(LOG_KBIBTEX_DATA) << "File objects being compared contain comments, ignoring those"; } else { /// This case should never be reached qCWarning(LOG_KBIBTEX_DATA) << "Met unhandled case while comparing two File objects"; return false; } } } } } return true; } bool File::operator!=(const File &other) const { return !operator ==(other); } const QSharedPointer File::containsKey(const QString &key, ElementTypes elementTypes) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "const QSharedPointer File::containsKey(const QString &key, ElementTypes elementTypes) const" << "This File object is not valid"; for (const auto &element : const_cast(*this)) { - const QSharedPointer entry = elementTypes.testFlag(etEntry) ? element.dynamicCast() : QSharedPointer(); + const QSharedPointer entry = elementTypes.testFlag(ElementType::Entry) ? element.dynamicCast() : QSharedPointer(); if (!entry.isNull()) { if (entry->id() == key) return entry; } else { - const QSharedPointer macro = elementTypes.testFlag(etMacro) ? element.dynamicCast() : QSharedPointer(); + const QSharedPointer macro = elementTypes.testFlag(ElementType::Macro) ? element.dynamicCast() : QSharedPointer(); if (!macro.isNull()) { if (macro->key() == key) return macro; } } } return QSharedPointer(); } QStringList File::allKeys(ElementTypes elementTypes) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::allKeys(ElementTypes elementTypes) const" << "This File object is not valid"; QStringList result; result.reserve(size()); for (const auto &element : const_cast(*this)) { - const QSharedPointer entry = elementTypes.testFlag(etEntry) ? element.dynamicCast() : QSharedPointer(); + const QSharedPointer entry = elementTypes.testFlag(ElementType::Entry) ? element.dynamicCast() : QSharedPointer(); if (!entry.isNull()) result.append(entry->id()); else { - const QSharedPointer macro = elementTypes.testFlag(etMacro) ? element.dynamicCast() : QSharedPointer(); + const QSharedPointer macro = elementTypes.testFlag(ElementType::Macro) ? element.dynamicCast() : QSharedPointer(); if (!macro.isNull()) result.append(macro->key()); } } return result; } QSet File::uniqueEntryValuesSet(const QString &fieldName) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QSet File::uniqueEntryValuesSet(const QString &fieldName) const" << "This File object is not valid"; QSet valueSet; const QString lcFieldName = fieldName.toLower(); for (const auto &element : const_cast(*this)) { const QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) if (it.key().toLower() == lcFieldName) { const auto itValue = it.value(); for (const QSharedPointer &valueItem : itValue) { /// Check if ValueItem to process points to a person const QSharedPointer person = valueItem.dynamicCast(); if (!person.isNull()) { QSet personNameFormattingSet {Preferences::personNameFormatLastFirst, Preferences::personNameFormatFirstLast}; personNameFormattingSet.insert(Preferences::instance().personNameFormat()); /// Add person's name formatted using each of the templates assembled above for (const QString &personNameFormatting : const_cast &>(personNameFormattingSet)) valueSet.insert(Person::transcribePersonName(person.data(), personNameFormatting)); } else { /// Default case: use PlainTextValue::text to translate ValueItem /// to a human-readable text valueSet.insert(PlainTextValue::text(*valueItem)); } } } } return valueSet; } QStringList File::uniqueEntryValuesList(const QString &fieldName) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::uniqueEntryValuesList(const QString &fieldName) const" << "This File object is not valid"; QSet valueSet = uniqueEntryValuesSet(fieldName); QStringList list = valueSet.toList(); list.sort(); return list; } void File::setProperty(const QString &key, const QVariant &value) { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "void File::setProperty(const QString &key, const QVariant &value)" << "This File object is not valid"; d->properties.insert(key, value); } QVariant File::property(const QString &key) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key) const" << "This File object is not valid"; return d->properties.contains(key) ? d->properties.value(key) : QVariant(); } QVariant File::property(const QString &key, const QVariant &defaultValue) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key, const QVariant &defaultValue) const" << "This File object is not valid"; return d->properties.value(key, defaultValue); } bool File::hasProperty(const QString &key) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "bool File::hasProperty(const QString &key) const" << "This File object is not valid"; return d->properties.contains(key); } void File::setPropertiesToDefault() { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "void File::setPropertiesToDefault()" << "This File object is not valid"; d->loadConfiguration(); } bool File::checkValidity() const { return d->checkValidity(); } QDebug operator<<(QDebug dbg, const File &file) { dbg.nospace() << "File is " << (file.checkValidity() ? "" : "NOT ") << "valid and has " << file.count() << " members"; return dbg; } diff --git a/src/data/file.h b/src/data/file.h index f172f575..4325f80a 100644 --- a/src/data/file.h +++ b/src/data/file.h @@ -1,126 +1,126 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 KBIBTEX_DATA_FILE_H #define KBIBTEX_DATA_FILE_H #include #include #include #ifdef HAVE_KF5 #include "kbibtexdata_export.h" #endif // HAVE_KF5 class Element; /** * This class represents a bibliographic file such as a BibTeX file * (or any other format after proper conversion). The file's content * can be accessed using the inherited QList interface (for example * list iterators). * @see Element * @author Thomas Fischer */ class KBIBTEXDATA_EXPORT File : public QList > { public: /// enum and flags to differ between entries, macros etc /// used for @see #allKeys() and @see #containsKey() - enum ElementType { - etEntry = 0x1, etMacro = 0x2, etAll = 0x3 + enum class ElementType { + Entry = 0x1, Macro = 0x2, All = Entry | Macro }; Q_DECLARE_FLAGS(ElementTypes, ElementType) /// used for property map const static QString Url; const static QString Encoding; const static QString StringDelimiter; const static QString QuoteComment; const static QString KeywordCasing; const static QString ProtectCasing; const static QString NameFormatting; const static QString ListSeparator; explicit File(); explicit File(const File &other); explicit File(File &&other); ~File(); /// Copy-assignment operator. File &operator= (const File &other); /// Move-assignment operator. File &operator= (File &&other); bool operator== (const File &other) const; bool operator!= (const File &other) const; /** * Check if a given key (e.g. a key for a macro or an id for an entry) * is contained in the file object. * @see #allKeys() const * @return @c the object addressed by the key @c, NULL if no such file has been found */ - const QSharedPointer containsKey(const QString &key, ElementTypes elementTypes = etAll) const; + const QSharedPointer containsKey(const QString &key, ElementTypes elementTypes = ElementType::All) const; /** * Retrieves a list of all keys for example from macros or entries. * @see #const containsKey(const QString &) const * @return list of keys */ - QStringList allKeys(ElementTypes elementTypes = etAll) const; + QStringList allKeys(ElementTypes elementTypes = ElementType::All) const; /** * Retrieves a set of all unique values (as text) for a specified * field from all entries * @param fieldName field name to scan, e.g. "volume" * @return list of unique values */ QSet uniqueEntryValuesSet(const QString &fieldName) const; /** * Retrieves a list of all unique values (as text) for a specified * field from all entries * @param fieldName field name to scan, e.g. "volume" * @return list of unique values */ QStringList uniqueEntryValuesList(const QString &fieldName) const; void setProperty(const QString &key, const QVariant &value); QVariant property(const QString &key) const; QVariant property(const QString &key, const QVariant &defaultValue) const; bool hasProperty(const QString &key) const; void setPropertiesToDefault(); /** * Check if this File object is valid by its own assessment. * No high-level checks, such as on the File instance's content, * are performed. * @return True if validity checks succeed, false otherwise */ bool checkValidity() const; private: class FilePrivate; FilePrivate *d; }; Q_DECLARE_METATYPE(File *) QDebug operator<<(QDebug dbg, const File &file); #endif // KBIBTEX_DATA_FILE_H diff --git a/src/data/value.cpp b/src/data/value.cpp index 5371aac4..123cee61 100644 --- a/src/data/value.cpp +++ b/src/data/value.cpp @@ -1,705 +1,705 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * 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) any later version. * * * * 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 "value.h" #include #include #include #include #include #ifdef HAVE_KF5 #include #include #include #else // HAVE_KF5 #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 #include #include "file.h" #include "logging_data.h" quint64 ValueItem::internalIdCounter = 0; uint qHash(const QSharedPointer &valueItem) { return qHash(valueItem->id()); } const QRegularExpression ValueItem::ignoredInSorting(QStringLiteral("[{}\\\\]+")); ValueItem::ValueItem() : internalId(++internalIdCounter) { /// nothing } ValueItem::~ValueItem() { /// nothing } quint64 ValueItem::id() const { return internalId; } bool ValueItem::operator!=(const ValueItem &other) const { return !operator ==(other); } Keyword::Keyword(const Keyword &other) : m_text(other.m_text) { /// nothing } Keyword::Keyword(const QString &text) : m_text(text) { /// nothing } void Keyword::setText(const QString &text) { m_text = text; } QString Keyword::text() const { return m_text; } void Keyword::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { - if (replaceMode == ValueItem::AnySubstring) + if (replaceMode == ValueItem::ReplaceMode::AnySubstring) m_text = m_text.replace(before, after); - else if (replaceMode == ValueItem::CompleteMatch && m_text == before) + else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before) m_text = after; } bool Keyword::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool Keyword::operator==(const ValueItem &other) const { const Keyword *otherKeyword = dynamic_cast(&other); if (otherKeyword != nullptr) { return otherKeyword->text() == text(); } else return false; } bool Keyword::isKeyword(const ValueItem &other) { return typeid(other) == typeid(Keyword); } Person::Person(const QString &firstName, const QString &lastName, const QString &suffix) : m_firstName(firstName), m_lastName(lastName), m_suffix(suffix) { /// nothing } Person::Person(const Person &other) : m_firstName(other.firstName()), m_lastName(other.lastName()), m_suffix(other.suffix()) { /// nothing } QString Person::firstName() const { return m_firstName; } QString Person::lastName() const { return m_lastName; } QString Person::suffix() const { return m_suffix; } void Person::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { - if (replaceMode == ValueItem::AnySubstring) { + if (replaceMode == ValueItem::ReplaceMode::AnySubstring) { m_firstName = m_firstName.replace(before, after); m_lastName = m_lastName.replace(before, after); m_suffix = m_suffix.replace(before, after); - } else if (replaceMode == ValueItem::CompleteMatch) { + } else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch) { if (m_firstName == before) m_firstName = after; if (m_lastName == before) m_lastName = after; if (m_suffix == before) m_suffix = after; } } bool Person::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString firstName = QString(m_firstName).remove(ignoredInSorting); const QString lastName = QString(m_lastName).remove(ignoredInSorting); const QString suffix = QString(m_suffix).remove(ignoredInSorting); return firstName.contains(pattern, caseSensitive) || lastName.contains(pattern, caseSensitive) || suffix.contains(pattern, caseSensitive) || QString(QStringLiteral("%1 %2|%2, %1")).arg(firstName, lastName).contains(pattern, caseSensitive); } bool Person::operator==(const ValueItem &other) const { const Person *otherPerson = dynamic_cast(&other); if (otherPerson != nullptr) { return otherPerson->firstName() == firstName() && otherPerson->lastName() == lastName() && otherPerson->suffix() == suffix(); } else return false; } QString Person::transcribePersonName(const Person *person, const QString &formatting) { return transcribePersonName(formatting, person->firstName(), person->lastName(), person->suffix()); } QString Person::transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix) { QString result = formatting; int p1 = -1, p2 = -1, p3 = -1; while ((p1 = result.indexOf('<')) >= 0 && (p2 = result.indexOf('>', p1 + 1)) >= 0 && (p3 = result.indexOf('%', p1)) >= 0 && p3 < p2) { QString insert; switch (result[p3 + 1].toLatin1()) { case 'f': insert = firstName; break; case 'l': insert = lastName; break; case 's': insert = suffix; break; } if (!insert.isEmpty()) insert = result.mid(p1 + 1, p3 - p1 - 1) + insert + result.mid(p3 + 2, p2 - p3 - 2); result = result.left(p1) + insert + result.mid(p2 + 1); } return result; } bool Person::isPerson(const ValueItem &other) { return typeid(other) == typeid(Person); } QDebug operator<<(QDebug dbg, const Person &person) { dbg.nospace() << "Person " << Person::transcribePersonName(&person, Preferences::defaultPersonNameFormat); return dbg; } MacroKey::MacroKey(const MacroKey &other) : m_text(other.m_text) { /// nothing } MacroKey::MacroKey(const QString &text) : m_text(text) { /// nothing } void MacroKey::setText(const QString &text) { m_text = text; } QString MacroKey::text() const { return m_text; } bool MacroKey::isValid() { const QString t = text(); static const QRegularExpression validMacroKey(QStringLiteral("^[a-z][-.:/+_a-z0-9]*$|^[0-9]+$"), QRegularExpression::CaseInsensitiveOption); const QRegularExpressionMatch match = validMacroKey.match(t); return match.hasMatch() && match.captured(0) == t; } void MacroKey::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { - if (replaceMode == ValueItem::AnySubstring) + if (replaceMode == ValueItem::ReplaceMode::AnySubstring) m_text = m_text.replace(before, after); - else if (replaceMode == ValueItem::CompleteMatch && m_text == before) + else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before) m_text = after; } bool MacroKey::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool MacroKey::operator==(const ValueItem &other) const { const MacroKey *otherMacroKey = dynamic_cast(&other); if (otherMacroKey != nullptr) { return otherMacroKey->text() == text(); } else return false; } bool MacroKey::isMacroKey(const ValueItem &other) { return typeid(other) == typeid(MacroKey); } QDebug operator<<(QDebug dbg, const MacroKey ¯okey) { dbg.nospace() << "MacroKey " << macrokey.text(); return dbg; } PlainText::PlainText(const PlainText &other) : m_text(other.text()) { /// nothing } PlainText::PlainText(const QString &text) : m_text(text) { /// nothing } void PlainText::setText(const QString &text) { m_text = text; } QString PlainText::text() const { return m_text; } void PlainText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { - if (replaceMode == ValueItem::AnySubstring) + if (replaceMode == ValueItem::ReplaceMode::AnySubstring) m_text = m_text.replace(before, after); - else if (replaceMode == ValueItem::CompleteMatch && m_text == before) + else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before) m_text = after; } bool PlainText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool PlainText::operator==(const ValueItem &other) const { const PlainText *otherPlainText = dynamic_cast(&other); if (otherPlainText != nullptr) { return otherPlainText->text() == text(); } else return false; } bool PlainText::isPlainText(const ValueItem &other) { return typeid(other) == typeid(PlainText); } QDebug operator<<(QDebug dbg, const PlainText &plainText) { dbg.nospace() << "PlainText " << plainText.text(); return dbg; } VerbatimText::VerbatimText(const VerbatimText &other) : m_text(other.text()) { /// nothing } VerbatimText::VerbatimText(const QString &text) : m_text(text) { /// nothing } void VerbatimText::setText(const QString &text) { m_text = text; } QString VerbatimText::text() const { return m_text; } void VerbatimText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { - if (replaceMode == ValueItem::AnySubstring) + if (replaceMode == ValueItem::ReplaceMode::AnySubstring) m_text = m_text.replace(before, after); - else if (replaceMode == ValueItem::CompleteMatch && m_text == before) + else if (replaceMode == ValueItem::ReplaceMode::CompleteMatch && m_text == before) m_text = after; } bool VerbatimText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); bool contained = text.contains(pattern, caseSensitive); if (!contained) { /// Only if simple text match failed, check color labels /// For a match, the user's pattern has to be the start of the color label /// and this verbatim text has to contain the color as hex string for (QVector>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); !contained && it != Preferences::instance().colorCodes().constEnd(); ++it) contained = text.compare(it->first.name(), Qt::CaseInsensitive) == 0 && it->second.contains(pattern, Qt::CaseInsensitive); } return contained; } bool VerbatimText::operator==(const ValueItem &other) const { const VerbatimText *otherVerbatimText = dynamic_cast(&other); if (otherVerbatimText != nullptr) { return otherVerbatimText->text() == text(); } else return false; } bool VerbatimText::isVerbatimText(const ValueItem &other) { return typeid(other) == typeid(VerbatimText); } QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText) { dbg.nospace() << "VerbatimText " << verbatimText.text(); return dbg; } Value::Value() : QVector >() { /// nothing } Value::Value(const Value &other) : QVector >(other) { /// nothing } Value::Value(Value &&other) : QVector >(other) { /// nothing } Value::~Value() { clear(); } void Value::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { QSet > unique; /// Delegate the replace operation to each individual ValueItem /// contained in this Value object for (Value::Iterator it = begin(); it != end();) { (*it)->replace(before, after, replaceMode); bool containedInUnique = false; for (const auto &valueItem : const_cast > &>(unique)) { containedInUnique = *valueItem.data() == *(*it).data(); if (containedInUnique) break; } if (containedInUnique) it = erase(it); else { unique.insert(*it); ++it; } } QSet uniqueValueItemTexts; for (int i = count() - 1; i >= 0; --i) { at(i)->replace(before, after, replaceMode); const QString valueItemText = PlainTextValue::text(*at(i).data()); if (uniqueValueItemTexts.contains(valueItemText)) { /// Due to a replace/delete operation above, an old ValueItem's text /// matches the replaced text. /// Therefore, remove the replaced text to avoid duplicates remove(i); ++i; /// compensate for for-loop's --i } else uniqueValueItemTexts.insert(valueItemText); } } void Value::replace(const QString &before, const QSharedPointer &after) { const QString valueText = PlainTextValue::text(*this); if (valueText == before) { clear(); append(after); } else { QSet uniqueValueItemTexts; for (int i = count() - 1; i >= 0; --i) { QString valueItemText = PlainTextValue::text(*at(i).data()); if (valueItemText == before) { /// Perform replacement operation QVector >::replace(i, after); valueItemText = PlainTextValue::text(*after.data()); // uniqueValueItemTexts.insert(PlainTextValue::text(*after.data())); } if (uniqueValueItemTexts.contains(valueItemText)) { /// Due to a previous replace operation, an existingValueItem's /// text matches a text which was inserted as an "after" ValueItem. /// Therefore, remove the old ValueItem to avoid duplicates. remove(i); } else { /// Neither a replacement, nor a duplicate. Keep this /// ValueItem (memorize as unique) and continue. uniqueValueItemTexts.insert(valueItemText); } } } } bool Value::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { for (const auto &valueItem : const_cast(*this)) { if (valueItem->containsPattern(pattern, caseSensitive)) return true; } return false; } bool Value::contains(const ValueItem &item) const { for (const auto &valueItem : const_cast(*this)) if (valueItem->operator==(item)) return true; return false; } Value &Value::operator=(const Value &rhs) { return static_cast(QVector >::operator =((rhs))); } Value &Value::operator=(Value &&rhs) { return static_cast(QVector >::operator =((rhs))); } Value &Value::operator<<(const QSharedPointer &value) { return static_cast(QVector >::operator<<((value))); } bool Value::operator==(const Value &rhs) const { const Value &lhs = *this; ///< just for readability to have a 'lhs' matching 'rhs' /// Obviously, both Values must be of same size if (lhs.count() != rhs.count()) return false; /// Synchronously iterate over both Values' ValueItems for (Value::ConstIterator lhsIt = lhs.constBegin(), rhsIt = rhs.constBegin(); lhsIt != lhs.constEnd() && rhsIt != rhs.constEnd(); ++lhsIt, ++rhsIt) { /// Are both ValueItems PlainTexts and are both PlainTexts equal? const QSharedPointer lhsPlainText = lhsIt->dynamicCast<PlainText>(); const QSharedPointer<PlainText> rhsPlainText = rhsIt->dynamicCast<PlainText>(); if ((lhsPlainText.isNull() && !rhsPlainText.isNull()) || (!lhsPlainText.isNull() && rhsPlainText.isNull())) return false; if (!lhsPlainText.isNull() && !rhsPlainText.isNull()) { if (*lhsPlainText.data() != *rhsPlainText.data()) return false; } else { /// Remainder of comparisons is like for PlainText above, just for other descendants of ValueItem const QSharedPointer<MacroKey> lhsMacroKey = lhsIt->dynamicCast<MacroKey>(); const QSharedPointer<MacroKey> rhsMacroKey = rhsIt->dynamicCast<MacroKey>(); if ((lhsMacroKey.isNull() && !rhsMacroKey.isNull()) || (!lhsMacroKey.isNull() && rhsMacroKey.isNull())) return false; if (!lhsMacroKey.isNull() && !rhsMacroKey.isNull()) { if (*lhsMacroKey.data() != *rhsMacroKey.data()) return false; } else { const QSharedPointer<Person> lhsPerson = lhsIt->dynamicCast<Person>(); const QSharedPointer<Person> rhsPerson = rhsIt->dynamicCast<Person>(); if ((lhsPerson.isNull() && !rhsPerson.isNull()) || (!lhsPerson.isNull() && rhsPerson.isNull())) return false; if (!lhsPerson.isNull() && !rhsPerson.isNull()) { if (*lhsPerson.data() != *rhsPerson.data()) return false; } else { const QSharedPointer<VerbatimText> lhsVerbatimText = lhsIt->dynamicCast<VerbatimText>(); const QSharedPointer<VerbatimText> rhsVerbatimText = rhsIt->dynamicCast<VerbatimText>(); if ((lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) || (!lhsVerbatimText.isNull() && rhsVerbatimText.isNull())) return false; if (!lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) { if (*lhsVerbatimText.data() != *rhsVerbatimText.data()) return false; } else { const QSharedPointer<Keyword> lhsKeyword = lhsIt->dynamicCast<Keyword>(); const QSharedPointer<Keyword> rhsKeyword = rhsIt->dynamicCast<Keyword>(); if ((lhsKeyword.isNull() && !rhsKeyword.isNull()) || (!lhsKeyword.isNull() && rhsKeyword.isNull())) return false; if (!lhsKeyword.isNull() && !rhsKeyword.isNull()) { if (*lhsKeyword.data() != *rhsKeyword.data()) return false; } else { /// If there are other descendants of ValueItem, add tests here ... return false; } } } } } } /// No check failed, so equalness is proven return true; } bool Value::operator!=(const Value &rhs) const { return !operator ==(rhs); } QDebug operator<<(QDebug dbg, const Value &value) { dbg.nospace() << "Value"; if (value.isEmpty()) dbg << " is empty"; else dbg.nospace() << ": " << PlainTextValue::text(value); return dbg; } QString PlainTextValue::text(const Value &value) { - ValueItemType vit = VITOther; - ValueItemType lastVit = VITOther; + ValueItemType vit = ValueItemType::Other; + ValueItemType lastVit = ValueItemType::Other; QString result; for (const auto &valueItem : value) { QString nextText = text(*valueItem, vit); if (!nextText.isEmpty()) { - if (lastVit == VITPerson && vit == VITPerson) + if (lastVit == ValueItemType::Person && vit == ValueItemType::Person) result.append(i18n(" and ")); // TODO proper list of authors/editors, not just joined by "and" - else if (lastVit == VITPerson && vit == VITOther && nextText == QStringLiteral("others")) { + else if (lastVit == ValueItemType::Person && vit == ValueItemType::Other && nextText == QStringLiteral("others")) { /// "and others" case: replace text to be appended by translated variant nextText = i18n(" and others"); - } else if (lastVit == VITKeyword && vit == VITKeyword) + } else if (lastVit == ValueItemType::Keyword && vit == ValueItemType::Keyword) result.append("; "); else if (!result.isEmpty()) result.append(" "); result.append(nextText); lastVit = vit; } } return result; } QString PlainTextValue::text(const QSharedPointer<const ValueItem> &valueItem) { const ValueItem *p = valueItem.data(); return text(*p); } QString PlainTextValue::text(const ValueItem &valueItem) { ValueItemType vit; return text(valueItem, vit); } QString PlainTextValue::text(const ValueItem &valueItem, ValueItemType &vit) { QString result; - vit = VITOther; + vit = ValueItemType::Other; bool isVerbatim = false; const PlainText *plainText = dynamic_cast<const PlainText *>(&valueItem); if (plainText != nullptr) { result = plainText->text(); } else { const MacroKey *macroKey = dynamic_cast<const MacroKey *>(&valueItem); if (macroKey != nullptr) { result = macroKey->text(); // TODO Use File to resolve key to full text } else { const Person *person = dynamic_cast<const Person *>(&valueItem); if (person != nullptr) { result = Person::transcribePersonName(person, Preferences::instance().personNameFormat()); - vit = VITPerson; + vit = ValueItemType::Person; } else { const Keyword *keyword = dynamic_cast<const Keyword *>(&valueItem); if (keyword != nullptr) { result = keyword->text(); - vit = VITKeyword; + vit = ValueItemType::Keyword; } else { const VerbatimText *verbatimText = dynamic_cast<const VerbatimText *>(&valueItem); if (verbatimText != nullptr) { result = verbatimText->text(); isVerbatim = true; } else qCWarning(LOG_KBIBTEX_DATA) << "Cannot interpret ValueItem to one of its descendants"; } } } } /// clean up result string const int len = result.length(); int j = 0; static const QChar cbo = QLatin1Char('{'), cbc = QLatin1Char('}'), bs = QLatin1Char('\\'), mns = QLatin1Char('-'), comma = QLatin1Char(','), thinspace = QChar(0x2009), tilde = QLatin1Char('~'), nobreakspace = QChar(0x00a0); for (int i = 0; i < len; ++i) { if ((result[i] == cbo || result[i] == cbc) && (i < 1 || result[i - 1] != bs)) { /// hop over curly brackets } else if (i < len - 1 && result[i] == bs && result[i + 1] == mns) { /// hop over hyphenation commands ++i; } else if (i < len - 1 && result[i] == bs && result[i + 1] == comma) { /// place '\,' with a thin space result[j] = thinspace; ++i; ++j; } else if (!isVerbatim && result[i] == tilde && (i < 1 || result[i - 1] != bs)) { /// replace '~' with a non-breaking space, /// except if text was verbatim (e.g. a 'localfile' value /// like '~/document.pdf' or 'document.pdf~') result[j] = nobreakspace; ++j; } else { if (i > j) { /// move individual characters forward in result string result[j] = result[i]; } ++j; } } result.resize(j); return result; } diff --git a/src/data/value.h b/src/data/value.h index 9547f0df..d9644c84 100644 --- a/src/data/value.h +++ b/src/data/value.h @@ -1,294 +1,294 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_DATA_VALUE_H #define KBIBTEX_DATA_VALUE_H #include <QVector> #include <QVariant> #include <QSharedPointer> #ifdef HAVE_KF5 #include "kbibtexdata_export.h" #endif // HAVE_KF5 class File; /** * Generic class of an information element in a @see Value object. * In BibTeX, ValueItems are concatenated by "#". */ class KBIBTEXDATA_EXPORT ValueItem { public: - enum ReplaceMode {CompleteMatch, AnySubstring}; + enum class ReplaceMode {CompleteMatch, AnySubstring}; ValueItem(); virtual ~ValueItem(); virtual void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) = 0; /** * Check if this object contains text pattern @p pattern. * @param pattern Pattern to check for * @param caseSensitive Case sensitivity setting for check * @return TRUE if pattern is contained within this value, otherwise FALSE */ virtual bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const = 0; /** * Compare to instance if they contain the same content. * Subclasses implement under which conditions two instances are equal. * Subclasses of different type are never equal. * @param other other instance to compare with * @return TRUE if both instances are equal */ virtual bool operator==(const ValueItem &other) const = 0; bool operator!=(const ValueItem &other) const; /** * Unique numeric identifier for every ValueItem instance. * @return Unique numeric identifier */ quint64 id() const; protected: /// contains text fragments to be removed before performing a "contains pattern" operation /// includes among other "{" and "}" static const QRegularExpression ignoredInSorting; private: /// Unique numeric identifier const quint64 internalId; /// Keeping track of next available unique numeric identifier static quint64 internalIdCounter; }; class KBIBTEXDATA_EXPORT Keyword: public ValueItem { public: Keyword(const Keyword &other); explicit Keyword(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a Keyword object. * @param other another ValueItem object to test * @return true if ValueItem is actually a Keyword */ static bool isKeyword(const ValueItem &other); protected: QString m_text; }; class KBIBTEXDATA_EXPORT Person: public ValueItem { public: /** * Create a representation for a person's name. In bibliographies, * a person is either an author or an editor. The four parameters * cover all common parts of a name. Only first and last name are * mandatory (each person should have those). @param firstName First name of a person. Example: "Peter" @param lastName Last name of a person. Example: "Smith" @param suffix Suffix after a name. Example: "jr." */ Person(const QString &firstName, const QString &lastName, const QString &suffix = QString()); Person(const Person &other); QString firstName() const; QString lastName() const; QString suffix() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; static QString transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix = QString()); static QString transcribePersonName(const Person *person, const QString &formatting); /** * Cheap and fast test if another ValueItem is a Person object. * @param other another ValueItem object to test * @return true if ValueItem is actually a Person */ static bool isPerson(const ValueItem &other); private: QString m_firstName; QString m_lastName; QString m_suffix; }; QDebug operator<<(QDebug dbg, const Person &person); Q_DECLARE_METATYPE(Person *) class KBIBTEXDATA_EXPORT MacroKey: public ValueItem { public: MacroKey(const MacroKey &other); explicit MacroKey(const QString &text); void setText(const QString &text); QString text() const; bool isValid(); void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a MacroKey object. * @param other another ValueItem object to test * @return true if ValueItem is actually a MacroKey */ static bool isMacroKey(const ValueItem &other); protected: QString m_text; }; QDebug operator<<(QDebug dbg, const MacroKey &macrokey); class KBIBTEXDATA_EXPORT PlainText: public ValueItem { public: PlainText(const PlainText &other); explicit PlainText(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a PlainText object. * @param other another ValueItem object to test * @return true if ValueItem is actually a PlainText */ static bool isPlainText(const ValueItem &other); protected: QString m_text; }; QDebug operator<<(QDebug dbg, const PlainText &plainText); class KBIBTEXDATA_EXPORT VerbatimText: public ValueItem { public: VerbatimText(const VerbatimText &other); explicit VerbatimText(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a VerbatimText object. * @param other another ValueItem object to test * @return true if ValueItem is actually a VerbatimText */ static bool isVerbatimText(const ValueItem &other); protected: QString m_text; private: #ifdef HAVE_KF5 struct ColorLabelPair { QString hexColor; QString label; }; static QList<ColorLabelPair> colorLabelPairs; static bool colorLabelPairsInitialized; #endif // HAVE_KF5 }; QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText); /** * Container class to hold values of BibTeX entry fields and similar value types in BibTeX file. * A Value object is built from a list of @see ValueItem objects. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Value: public QVector<QSharedPointer<ValueItem> > { public: Value(); Value(const Value &other); Value(Value &&other); virtual ~Value(); void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode); void replace(const QString &before, const QSharedPointer<ValueItem> &after); /** * Check if this value contains text pattern @p pattern. * @param pattern Pattern to check for * @param caseSensitive Case sensitivity setting for check * @return TRUE if pattern is contained within this value, otherwise FALSE */ bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const; bool contains(const ValueItem &item) const; Value &operator=(const Value &rhs); Value &operator=(Value &&rhs); Value &operator<<(const QSharedPointer<ValueItem> &value); bool operator==(const Value &rhs) const; bool operator!=(const Value &rhs) const; }; QDebug operator<<(QDebug dbg, const Value &value); class KBIBTEXDATA_EXPORT PlainTextValue { public: static QString text(const Value &value); static QString text(const ValueItem &valueItem); static QString text(const QSharedPointer<const ValueItem> &valueItem); private: - enum ValueItemType { VITOther = 0, VITPerson, VITKeyword}; + enum class ValueItemType { Other = 0, Person, Keyword}; static QString text(const ValueItem &valueItem, ValueItemType &vit); }; Q_DECLARE_METATYPE(Value) #endif // KBIBTEX_DATA_VALUE_H diff --git a/src/global/kbibtex.cpp b/src/global/kbibtex.cpp index 402a15c0..cb297a9d 100644 --- a/src/global/kbibtex.cpp +++ b/src/global/kbibtex.cpp @@ -1,85 +1,127 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "kbibtex.h" #include <QString> #include <QRegularExpression> +#include <QHash> +#include <QDebug> const QString KBibTeX::extensionTeX = QStringLiteral(".tex"); const QString KBibTeX::extensionAux = QStringLiteral(".aux"); const QString KBibTeX::extensionBBL = QStringLiteral(".bbl"); const QString KBibTeX::extensionBLG = QStringLiteral(".blg"); const QString KBibTeX::extensionBibTeX = QStringLiteral(".bib"); const QString KBibTeX::extensionPDF = QStringLiteral(".pdf"); const QString KBibTeX::extensionPostScript = QStringLiteral(".ps"); const QString KBibTeX::extensionRTF = QStringLiteral(".rtf"); const QString KBibTeX::Months[] = { QStringLiteral("January"), QStringLiteral("February"), QStringLiteral("March"), QStringLiteral("April"), QStringLiteral("May"), QStringLiteral("June"), QStringLiteral("July"), QStringLiteral("August"), QStringLiteral("September"), QStringLiteral("October"), QStringLiteral("November"), QStringLiteral("December") }; const QString KBibTeX::MonthsTriple[] = { QStringLiteral("jan"), QStringLiteral("feb"), QStringLiteral("mar"), QStringLiteral("apr"), QStringLiteral("may"), QStringLiteral("jun"), QStringLiteral("jul"), QStringLiteral("aug"), QStringLiteral("sep"), QStringLiteral("oct"), QStringLiteral("nov"), QStringLiteral("dec") }; const QRegularExpression KBibTeX::fileListSeparatorRegExp(QStringLiteral("[ \\t]*[;\\n]+[ \\t]*")); const QRegularExpression KBibTeX::fileRegExp(QStringLiteral("(\\bfile:)?[^{}\\t]+\\.\\w{2,4}\\b"), QRegularExpression::CaseInsensitiveOption); const QRegularExpression KBibTeX::urlRegExp(QStringLiteral("\\b(http|s?ftp|webdav|file)s?://[^ {}\"]+(\\b|[/])"), QRegularExpression::CaseInsensitiveOption); const QRegularExpression KBibTeX::doiRegExp(QStringLiteral("10([.][0-9]+)+/[/-a-z0-9.()<>_:;\\\\]+"), QRegularExpression::CaseInsensitiveOption); const QRegularExpression KBibTeX::arXivRegExpWithPrefix(QStringLiteral("arXiv:(([0-9]+[.][0-9]+|[a-z-]+/[0-9]+)(v[0-9]+)?)")); const QRegularExpression KBibTeX::arXivRegExpWithoutPrefix(QStringLiteral("([0-9]+[.][0-9]+|[a-z-]+/[0-9]+)(v[0-9]+)?")); const QRegularExpression KBibTeX::mendeleyFileRegExp(QStringLiteral(":(.*):pdf"), QRegularExpression::CaseInsensitiveOption); const QRegularExpression KBibTeX::domainNameRegExp(QStringLiteral("[a-z0-9.-]+\\.((a[cdefgilmnoqrstuwxz]|aero|arpa)|(b[abdefghijmnorstvwyz]|biz)|(c[acdfghiklmnorsuvxyz]|cat|com|coop)|d[ejkmoz]|(e[ceghrstu]|edu)|f[ijkmor]|(g[abdefghilmnpqrstuwy]|gov)|h[kmnrtu]|(i[delmnoqrst]|info|int)|(j[emop]|jobs)|k[eghimnprwyz]|l[abcikrstuvy]|(m[acdghklmnopqrstuvwxyz]|me|mil|mobi|museum)|(n[acefgilopruz]|name|net)|(om|org)|(p[aefghklmnrstwy]|pro)|qa|r[eouw]|s[abcdeghijklmnortvyz]|(t[cdfghjklmnoprtvwz]|travel)|u[agkmsyz]|v[aceginu]|w[fs]|y[etu]|z[amw])\\b"), QRegularExpression::CaseInsensitiveOption); const QRegularExpression KBibTeX::htmlRegExp(QStringLiteral("</?(a|pre|p|br|span|i|b|italic)\\b[^>{}]{,32}>"), QRegularExpression::CaseInsensitiveOption); const QString KBibTeX::doiUrlPrefix(QStringLiteral("https://dx.doi.org/")); ///< for internal use in FileInfo::doiUrlPrefix, use FileInfo::doiUrlPrefix() instead bool KBibTeX::isLocalOrRelative(const QUrl &url) { return url.isLocalFile() || url.isRelative() || url.scheme().isEmpty(); } /** * Poor man's variant of a text-squeezing function. * Effect is similar as observed in KSqueezedTextLabel: * If the text is longer as n characters, the middle part * will be cut away and replaced by "..." to get a * string of max n characters. */ QString KBibTeX::squeezeText(const QString &text, int n) { return text.length() <= n ? text : text.left(n / 2 - 1) + QStringLiteral("...") + text.right(n / 2 - 2); } QString KBibTeX::leftSqueezeText(const QString &text, int n) { return text.length() <= n ? text : text.left(n) + QStringLiteral("..."); } int KBibTeX::validateCurlyBracketContext(const QString &text) { int openingCB = 0, closingCB = 0; for (int i = 0; i < text.length(); ++i) { if (i == 0 || text[i - 1] != QLatin1Char('\\')) { if (text[i] == QLatin1Char('{')) ++openingCB; else if (text[i] == QLatin1Char('}')) ++closingCB; } } return openingCB - closingCB; } + +QDebug operator<<(QDebug dbg, const KBibTeX::TypeFlag &typeFlag) +{ + static const auto pairs = QHash<int, const char *> { + {static_cast<int>(KBibTeX::TypeFlag::Invalid), "Invalid"}, + {static_cast<int>(KBibTeX::TypeFlag::PlainText), "PlainText"}, + {static_cast<int>(KBibTeX::TypeFlag::Reference), "Reference"}, + {static_cast<int>(KBibTeX::TypeFlag::Person), "Person"}, + {static_cast<int>(KBibTeX::TypeFlag::Keyword), "Keyword"}, + {static_cast<int>(KBibTeX::TypeFlag::Verbatim), "Verbatim"}, + {static_cast<int>(KBibTeX::TypeFlag::Source), "Source"} + }; + dbg.nospace(); + const int typeFlagInt = static_cast<int>(typeFlag); + dbg << (pairs.contains(typeFlagInt) ? pairs[typeFlagInt] : "???"); + return dbg; +} + +QDebug operator<<(QDebug dbg, const KBibTeX::TypeFlags &typeFlags) +{ + static const auto pairs = QHash<const char *, KBibTeX::TypeFlag> { + {"Invalid", KBibTeX::TypeFlag::Invalid}, + {"PlainText", KBibTeX::TypeFlag::PlainText}, + {"Reference", KBibTeX::TypeFlag::Reference}, + {"Person", KBibTeX::TypeFlag::Person}, + {"Keyword", KBibTeX::TypeFlag::Keyword}, + {"Verbatim", KBibTeX::TypeFlag::Verbatim}, + {"Source", KBibTeX::TypeFlag::Source} + }; + dbg.nospace(); + bool first = true; + for (QHash<const char *, KBibTeX::TypeFlag>::ConstIterator it = pairs.constBegin(); it != pairs.constEnd(); ++it) { + if (typeFlags.testFlag(it.value())) { + if (first) dbg << "|"; + dbg << it.key(); + first = false; + } + } + return dbg; +} diff --git a/src/global/kbibtex.h b/src/global/kbibtex.h index 8adb3637..63247d79 100644 --- a/src/global/kbibtex.h +++ b/src/global/kbibtex.h @@ -1,110 +1,95 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_GLOBAL_KBIBTEX_H #define KBIBTEX_GLOBAL_KBIBTEX_H #include <QMap> #include <QUrl> #ifdef HAVE_KF5 #include "kbibtexglobal_export.h" #endif // HAVE_KF5 #define squeeze_text(text, n) ((text).length()<=(n)?(text):(text).left((n)/2-1)+QStringLiteral("...")+(text).right((n)/2-2)) /** @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXGLOBAL_EXPORT KBibTeX { public: static const QString extensionTeX; static const QString extensionAux; static const QString extensionBBL; static const QString extensionBLG; static const QString extensionBibTeX; static const QString extensionPDF; static const QString extensionPostScript; static const QString extensionRTF; - enum Casing { - cLowerCase = 0, - cInitialCapital = 1, - cUpperCamelCase = 2, - cLowerCamelCase = 3, - cUpperCase = 4 - }; - - enum FieldInputType { - SingleLine = 1, - MultiLine = 2, - List = 3, - URL = 4, - Month = 5, - Color = 6, - PersonList = 7, - UrlList = 8, - KeywordList = 9, - CrossRef = 10, - StarRating = 11, - Edition = 12 - }; - - enum TypeFlag { - tfInvalid = 0x0, - tfPlainText = 0x1, - tfReference = 0x2, - tfPerson = 0x4, - tfKeyword = 0x8, - tfVerbatim = 0x10, - tfSource = 0x100 + enum class Casing { LowerCase, InitialCapital, UpperCamelCase, LowerCamelCase, UpperCase }; + + enum class FieldInputType { SingleLine, MultiLine, List, Url, Month, Color, PersonList, UrlList, KeywordList, CrossRef, StarRating, Edition }; + + enum class TypeFlag { + Invalid = 0x0, + PlainText = 0x1, + Reference = 0x2, + Person = 0x4, + Keyword = 0x8, + Verbatim = 0x10, + Source = 0x100 }; Q_DECLARE_FLAGS(TypeFlags, TypeFlag) static const QString Months[]; static const QString MonthsTriple[]; static const QRegularExpression fileListSeparatorRegExp; static const QRegularExpression fileRegExp; static const QRegularExpression urlRegExp; static const QRegularExpression doiRegExp; static const QRegularExpression arXivRegExpWithPrefix; static const QRegularExpression arXivRegExpWithoutPrefix; static const QRegularExpression mendeleyFileRegExp; static const QRegularExpression domainNameRegExp; static const QRegularExpression htmlRegExp; static const QString doiUrlPrefix; ///< use FileInfo::doiUrlPrefix() instead static bool isLocalOrRelative(const QUrl &url); /** * Poor man's variant of a text-squeezing function. * Effect is similar as observed in KSqueezedTextLabel: * If the text is longer as n characters, the middle part * will be cut away and replaced by "..." to get a * string of max n characters. */ static QString squeezeText(const QString &text, int n); static QString leftSqueezeText(const QString &text, int n); static int validateCurlyBracketContext(const QString &text); + }; Q_DECLARE_OPERATORS_FOR_FLAGS(KBibTeX::TypeFlags) +KBIBTEXGLOBAL_EXPORT QDebug operator<<(QDebug dbg, const KBibTeX::TypeFlag &typeFlag); +KBIBTEXGLOBAL_EXPORT QDebug operator<<(QDebug dbg, const KBibTeX::TypeFlags &typeFlags); + #endif // KBIBTEX_GLOBAL_KBIBTEX_H diff --git a/src/gui/config/entrylayout.cpp b/src/gui/config/entrylayout.cpp index ba1bf7d5..42126806 100644 --- a/src/gui/config/entrylayout.cpp +++ b/src/gui/config/entrylayout.cpp @@ -1,147 +1,147 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "entrylayout.h" #include <QStandardPaths> #include <KSharedConfig> #include <KConfigGroup> #include <KLocalizedString> #include <Preferences> #include "logging_gui.h" static const int entryLayoutMaxTabCount = 256; static const int entryLayoutMaxFieldPerTabCount = 256; class EntryLayout::EntryLayoutPrivate { public: EntryLayout *p; EntryLayoutPrivate(EntryLayout *parent) : p(parent) { /// nothing } static QString convert(KBibTeX::FieldInputType fil) { switch (fil) { - case KBibTeX::SingleLine : return QStringLiteral("SingleLine"); - case KBibTeX::MultiLine : return QStringLiteral("MultiLine"); - case KBibTeX::List : return QStringLiteral("List"); - case KBibTeX::URL : return QStringLiteral("URL"); - case KBibTeX::Month : return QStringLiteral("Month"); - case KBibTeX::Edition : return QStringLiteral("Edition"); - case KBibTeX::Color : return QStringLiteral("Color"); - case KBibTeX::PersonList : return QStringLiteral("PersonList"); - case KBibTeX::KeywordList : return QStringLiteral("KeywordList"); - case KBibTeX::CrossRef : return QStringLiteral("CrossRef"); - case KBibTeX::StarRating : return QStringLiteral("StarRating"); - case KBibTeX::UrlList : return QStringLiteral("UrlList"); + case KBibTeX::FieldInputType::SingleLine : return QStringLiteral("SingleLine"); + case KBibTeX::FieldInputType::MultiLine : return QStringLiteral("MultiLine"); + case KBibTeX::FieldInputType::List : return QStringLiteral("List"); + case KBibTeX::FieldInputType::Url : return QStringLiteral("URL"); + case KBibTeX::FieldInputType::Month : return QStringLiteral("Month"); + case KBibTeX::FieldInputType::Edition : return QStringLiteral("Edition"); + case KBibTeX::FieldInputType::Color : return QStringLiteral("Color"); + case KBibTeX::FieldInputType::PersonList : return QStringLiteral("PersonList"); + case KBibTeX::FieldInputType::KeywordList : return QStringLiteral("KeywordList"); + case KBibTeX::FieldInputType::CrossRef : return QStringLiteral("CrossRef"); + case KBibTeX::FieldInputType::StarRating : return QStringLiteral("StarRating"); + case KBibTeX::FieldInputType::UrlList : return QStringLiteral("UrlList"); } return QString(); } static KBibTeX::FieldInputType convert(const QString &text) { if (text == QStringLiteral("List")) - return KBibTeX::List; + return KBibTeX::FieldInputType::List; else if (text == QStringLiteral("MultiLine")) - return KBibTeX::MultiLine; + return KBibTeX::FieldInputType::MultiLine; else if (text == QStringLiteral("URL")) - return KBibTeX::URL; + return KBibTeX::FieldInputType::Url; else if (text == QStringLiteral("UrlList")) - return KBibTeX::UrlList; + return KBibTeX::FieldInputType::UrlList; else if (text == QStringLiteral("Month")) - return KBibTeX::Month; + return KBibTeX::FieldInputType::Month; else if (text == QStringLiteral("Edition")) - return KBibTeX::Edition; + return KBibTeX::FieldInputType::Edition; else if (text == QStringLiteral("Color")) - return KBibTeX::Color; + return KBibTeX::FieldInputType::Color; else if (text == QStringLiteral("PersonList")) - return KBibTeX::PersonList; + return KBibTeX::FieldInputType::PersonList; else if (text == QStringLiteral("KeywordList")) - return KBibTeX::KeywordList; + return KBibTeX::FieldInputType::KeywordList; else if (text == QStringLiteral("CrossRef")) - return KBibTeX::CrossRef; + return KBibTeX::FieldInputType::CrossRef; else if (text == QStringLiteral("StarRating")) - return KBibTeX::StarRating; + return KBibTeX::FieldInputType::StarRating; else - return KBibTeX::SingleLine; + return KBibTeX::FieldInputType::SingleLine; } void load(const QString &style) { p->clear(); const QString stylefile = QStringLiteral("kbibtex/") + style + QStringLiteral(".kbstyle"); KSharedConfigPtr layoutConfig = KSharedConfig::openConfig(stylefile, KConfig::FullConfig, QStandardPaths::GenericDataLocation); static const QString groupName = QStringLiteral("EntryLayoutTab"); const KConfigGroup configGroup(layoutConfig, groupName); const int tabCount = qMin(configGroup.readEntry("count", 0), entryLayoutMaxTabCount); for (int tab = 1; tab <= tabCount; ++tab) { const QString groupName = QString(QStringLiteral("EntryLayoutTab%1")).arg(tab); const KConfigGroup configGroup(layoutConfig, groupName); QSharedPointer<EntryTabLayout> etl = QSharedPointer<EntryTabLayout>(new EntryTabLayout); etl->identifier = configGroup.readEntry("identifier", QString(QStringLiteral("etl%1")).arg(tab)); etl->uiCaption = i18n(configGroup.readEntry("uiCaption", QString()).toUtf8().constData()); etl->iconName = configGroup.readEntry("iconName", "entry"); etl->columns = configGroup.readEntry("columns", 1); if (etl->uiCaption.isEmpty()) continue; const int fieldCount = qMin(configGroup.readEntry("count", 0), entryLayoutMaxFieldPerTabCount); for (int field = 1; field <= fieldCount; ++field) { SingleFieldLayout sfl; sfl.bibtexLabel = configGroup.readEntry(QString(QStringLiteral("bibtexLabel%1")).arg(field), QString()); sfl.uiLabel = i18n(configGroup.readEntry(QString(QStringLiteral("uiLabel%1")).arg(field), QString()).toUtf8().constData()); sfl.fieldInputLayout = EntryLayoutPrivate::convert(configGroup.readEntry(QString(QStringLiteral("fieldInputLayout%1")).arg(field), "SingleLine")); if (sfl.bibtexLabel.isEmpty() || sfl.uiLabel.isEmpty()) continue; etl->singleFieldLayouts.append(sfl); } const QString infoMessagePipeSeparated = i18n(configGroup.readEntry("infoMessage", QString()).toUtf8().constData()); if (!infoMessagePipeSeparated.isEmpty()) etl->infoMessages = infoMessagePipeSeparated.split(QLatin1Char('|')); p->append(etl); } if (p->isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "List of entry layouts is empty"; } }; EntryLayout::EntryLayout(const QString &style) : QVector<QSharedPointer<EntryTabLayout> >(), d(new EntryLayoutPrivate(this)) { d->load(style); } EntryLayout::~EntryLayout() { delete d; } const EntryLayout &EntryLayout::instance() { static const EntryLayout singletonBibTeX(QStringLiteral("bibtex")), singletonBibLaTeX(QStringLiteral("biblatex")); - return Preferences::instance().bibliographySystem() == Preferences::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; + return Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibLaTeX ? singletonBibLaTeX : singletonBibTeX; } diff --git a/src/gui/element/associatedfilesui.cpp b/src/gui/element/associatedfilesui.cpp index 8eaacd05..d90d61ea 100644 --- a/src/gui/element/associatedfilesui.cpp +++ b/src/gui/element/associatedfilesui.cpp @@ -1,274 +1,274 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "associatedfilesui.h" #include <QLayout> #include <QLabel> #include <QGroupBox> #include <QRadioButton> #include <QButtonGroup> #include <QDir> #include <QDialog> #include <QDialogButtonBox> #include <QPushButton> #include <QLineEdit> #include <QPointer> #include <KLocalizedString> class AssociatedFilesUI::Private { private: AssociatedFilesUI *p; public: QLabel *labelGreeting; QLineEdit *lineEditSourceUrl; QRadioButton *radioNoCopyMove, *radioCopyFile, *radioMoveFile; QLabel *labelMoveCopyLocation; QLineEdit *lineMoveCopyLocation; QGroupBox *groupBoxRename; QRadioButton *radioKeepFilename, *radioRenameToEntryId, *radioUserDefinedName; QLineEdit *lineEditUserDefinedName; QGroupBox *groupBoxPathType; QRadioButton *radioRelativePath, *radioAbsolutePath; QLineEdit *linePreview; QUrl sourceUrl; QSharedPointer<Entry> entry; QString entryId; const File *bibTeXfile; Private(AssociatedFilesUI *parent) : p(parent), entry(QSharedPointer<Entry>()), bibTeXfile(nullptr) { setupGUI(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); labelGreeting = new QLabel(p); layout->addWidget(labelGreeting); labelGreeting->setWordWrap(true); lineEditSourceUrl = new QLineEdit(p); layout->addWidget(lineEditSourceUrl); lineEditSourceUrl->setReadOnly(true); layout->addSpacing(8); QLabel *label = new QLabel(i18n("The following operations can be performed when associating the document with the entry:"), p); layout->addWidget(label); label->setWordWrap(true); QGroupBox *groupBox = new QGroupBox(i18n("File operation"), p); layout->addWidget(groupBox); QBoxLayout *groupBoxLayout = new QVBoxLayout(groupBox); QButtonGroup *buttonGroup = new QButtonGroup(groupBox); radioNoCopyMove = new QRadioButton(i18n("Do not copy or move document, only insert reference to it"), groupBox); groupBoxLayout->addWidget(radioNoCopyMove); buttonGroup->addButton(radioNoCopyMove); radioCopyFile = new QRadioButton(i18n("Copy document next to bibliography file"), groupBox); groupBoxLayout->addWidget(radioCopyFile); buttonGroup->addButton(radioCopyFile); radioMoveFile = new QRadioButton(i18n("Move document next to bibliography file"), groupBox); groupBoxLayout->addWidget(radioMoveFile); buttonGroup->addButton(radioMoveFile); connect(buttonGroup, static_cast<void(QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioNoCopyMove->setChecked(true); /// by default groupBoxLayout->addSpacing(4); labelMoveCopyLocation = new QLabel(i18n("Path and filename of bibliography file:"), groupBox); groupBoxLayout->addWidget(labelMoveCopyLocation, 1); lineMoveCopyLocation = new QLineEdit(groupBox); lineMoveCopyLocation->setReadOnly(true); groupBoxLayout->addWidget(lineMoveCopyLocation, 1); groupBoxRename = new QGroupBox(i18n("Rename Document?"), p); layout->addWidget(groupBoxRename); QGridLayout *gridLayout = new QGridLayout(groupBoxRename); gridLayout->setColumnMinimumWidth(0, 16); gridLayout->setColumnStretch(0, 0); gridLayout->setColumnStretch(1, 1); buttonGroup = new QButtonGroup(groupBoxRename); radioKeepFilename = new QRadioButton(i18n("Keep document's original filename"), groupBoxRename); gridLayout->addWidget(radioKeepFilename, 0, 0, 1, 2); buttonGroup->addButton(radioKeepFilename); radioRenameToEntryId = new QRadioButton(groupBoxRename); gridLayout->addWidget(radioRenameToEntryId, 1, 0, 1, 2); buttonGroup->addButton(radioRenameToEntryId); radioUserDefinedName = new QRadioButton(i18n("User-defined name:"), groupBoxRename); gridLayout->addWidget(radioUserDefinedName, 2, 0, 1, 2); buttonGroup->addButton(radioUserDefinedName); lineEditUserDefinedName = new QLineEdit(groupBoxRename); gridLayout->addWidget(lineEditUserDefinedName, 3, 1, 1, 1); connect(lineEditUserDefinedName, &QLineEdit::textEdited, p, &AssociatedFilesUI::updateUIandPreview); connect(buttonGroup, static_cast<void(QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioRenameToEntryId->setChecked(true); /// by default groupBoxPathType = new QGroupBox(i18n("Path as Inserted into Entry"), p); buttonGroup = new QButtonGroup(groupBoxPathType); layout->addWidget(groupBoxPathType); groupBoxLayout = new QVBoxLayout(groupBoxPathType); radioRelativePath = new QRadioButton(i18n("Relative Path"), groupBoxPathType); groupBoxLayout->addWidget(radioRelativePath); buttonGroup->addButton(radioRelativePath); radioAbsolutePath = new QRadioButton(i18n("Absolute Path"), groupBoxPathType); groupBoxLayout->addWidget(radioAbsolutePath); buttonGroup->addButton(radioAbsolutePath); connect(buttonGroup, static_cast<void(QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioRelativePath->setChecked(true); /// by default layout->addSpacing(8); label = new QLabel(i18n("Preview of reference to be inserted:"), p); layout->addWidget(label); linePreview = new QLineEdit(p); layout->addWidget(linePreview); linePreview->setReadOnly(true); layout->addStretch(10); } }; QString AssociatedFilesUI::associateUrl(const QUrl &url, QSharedPointer<Entry> &entry, const File *bibTeXfile, const bool doInsertUrl, QWidget *parent) { QPointer<QDialog> dlg = new QDialog(parent); QBoxLayout *layout = new QVBoxLayout(dlg); QPointer<AssociatedFilesUI> ui = new AssociatedFilesUI(entry->id(), bibTeXfile, dlg); layout->addWidget(ui); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); dlg->setLayout(layout); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); if (url.isLocalFile()) ui->setupForLocalFile(url, entry->id()); else ui->setupForRemoteUrl(url, entry->id()); const bool accepted = dlg->exec() == QDialog::Accepted; bool success = false; QString referenceString; if (accepted) { const QUrl newUrl = AssociatedFiles::copyDocument(url, entry->id(), bibTeXfile, ui->renameOperation(), ui->moveCopyOperation(), dlg, ui->userDefinedFilename()); success = newUrl.isValid(); if (success) { referenceString = doInsertUrl ? AssociatedFiles::insertUrl(newUrl, entry, bibTeXfile, ui->pathType()) : AssociatedFiles::computeAssociateUrl(newUrl, bibTeXfile, ui->pathType()); success &= !referenceString.isEmpty(); } } delete dlg; return success ? referenceString : QString(); } AssociatedFilesUI::AssociatedFilesUI(const QString &entryId, const File *bibTeXfile, QWidget *parent) : QWidget(parent), d(new AssociatedFilesUI::Private(this)) { d->entryId = entryId; d->bibTeXfile = bibTeXfile; } AssociatedFilesUI::~AssociatedFilesUI() { delete d; } AssociatedFiles::RenameOperation AssociatedFilesUI::renameOperation() const { if (d->radioRenameToEntryId->isChecked()) - return AssociatedFiles::roEntryId; + return AssociatedFiles::RenameOperation::EntryId; else if (d->radioKeepFilename->isChecked() || d->lineEditUserDefinedName->text().isEmpty()) - return AssociatedFiles::roKeepName; + return AssociatedFiles::RenameOperation::KeepName; else - return AssociatedFiles::roUserDefined; + return AssociatedFiles::RenameOperation::UserDefined; } AssociatedFiles::MoveCopyOperation AssociatedFilesUI::moveCopyOperation() const { - if (d->radioNoCopyMove->isChecked()) return AssociatedFiles::mcoNoCopyMove; - else if (d->radioMoveFile->isChecked()) return AssociatedFiles::mcoMove; - else return AssociatedFiles::mcoCopy; + if (d->radioNoCopyMove->isChecked()) return AssociatedFiles::MoveCopyOperation::None; + else if (d->radioMoveFile->isChecked()) return AssociatedFiles::MoveCopyOperation::Move; + else return AssociatedFiles::MoveCopyOperation::Copy; } AssociatedFiles::PathType AssociatedFilesUI::pathType() const { - return d->radioAbsolutePath->isChecked() ? AssociatedFiles::ptAbsolute : AssociatedFiles::ptRelative; + return d->radioAbsolutePath->isChecked() ? AssociatedFiles::PathType::Absolute : AssociatedFiles::PathType::Relative; } QString AssociatedFilesUI::userDefinedFilename() const { QString text = d->lineEditUserDefinedName->text(); const int p = qMax(text.lastIndexOf(QLatin1Char('/')), text.lastIndexOf(QDir::separator())); if (p > 0) text = text.mid(p + 1); return text; } void AssociatedFilesUI::updateUIandPreview() { QString preview = i18n("No preview available"); const QString entryId = d->entryId.isEmpty() && !d->entry.isNull() ? d->entry->id() : d->entryId; if (entryId.isEmpty()) { d->radioRenameToEntryId->setEnabled(false); d->radioKeepFilename->setChecked(true); } else d->radioRenameToEntryId->setEnabled(true); if (d->bibTeXfile == nullptr || !d->bibTeXfile->hasProperty(File::Url)) { d->radioRelativePath->setEnabled(false); d->radioAbsolutePath->setChecked(true); d->labelMoveCopyLocation->hide(); d->lineMoveCopyLocation->hide(); } else { d->radioRelativePath->setEnabled(true); d->labelMoveCopyLocation->show(); d->lineMoveCopyLocation->show(); d->lineMoveCopyLocation->setText(d->bibTeXfile->property(File::Url).toUrl().path()); } if (d->bibTeXfile != nullptr && d->sourceUrl.isValid() && !entryId.isEmpty()) { const QPair<QUrl, QUrl> newURLs = AssociatedFiles::computeSourceDestinationUrls(d->sourceUrl, entryId, d->bibTeXfile, renameOperation(), d->lineEditUserDefinedName->text()); if (newURLs.second.isValid()) preview = AssociatedFiles::computeAssociateUrl(newURLs.second, d->bibTeXfile, pathType()); } d->linePreview->setText(preview); d->groupBoxRename->setEnabled(!d->radioNoCopyMove->isChecked()); } void AssociatedFilesUI::setupForRemoteUrl(const QUrl &url, const QString &entryId) { d->sourceUrl = url; d->lineEditSourceUrl->setText(url.toDisplayString()); if (entryId.isEmpty()) { d->labelGreeting->setText(i18n("The following remote document is about to be associated with the current entry:")); d->radioRenameToEntryId->setText(i18n("Rename after entry's id")); } else { d->labelGreeting->setText(i18n("The following remote document is about to be associated with entry '%1':", entryId)); d->radioRenameToEntryId->setText(i18n("Rename after entry's id: '%1'", entryId)); } updateUIandPreview(); } void AssociatedFilesUI::setupForLocalFile(const QUrl &url, const QString &entryId) { d->sourceUrl = url; d->lineEditSourceUrl->setText(url.path()); if (entryId.isEmpty()) { d->labelGreeting->setText(i18n("The following local document is about to be associated with the current entry:")); d->radioRenameToEntryId->setText(i18n("Rename after entry's id")); } else { d->labelGreeting->setText(i18n("The following local document is about to be associated with entry '%1':", entryId)); d->radioRenameToEntryId->setText(i18n("Rename after entry's id: '%1'", entryId)); } updateUIandPreview(); } diff --git a/src/gui/element/elementeditor.cpp b/src/gui/element/elementeditor.cpp index 349f6516..4599264a 100644 --- a/src/gui/element/elementeditor.cpp +++ b/src/gui/element/elementeditor.cpp @@ -1,687 +1,687 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "elementeditor.h" #include <typeinfo> #include <QCheckBox> #include <QLabel> #include <QLayout> #include <QBuffer> #include <QTextStream> #include <QApplication> #include <QFileInfo> #include <QMenu> #include <QScrollArea> #include <QPushButton> #include <KMessageBox> #include <KLocalizedString> #include <KSharedConfig> #include <KConfigGroup> #include <NotificationHub> #include <Entry> #include <Comment> #include <Macro> #include <Preamble> #include <Element> #include <File> #include <CheckBibTeX> #include "elementwidgets.h" #include "widgets/hidingtabwidget.h" #include "widgets/menulineedit.h" class ElementEditor::ElementEditorPrivate : public ElementEditor::ApplyElementInterface { private: const File *file; QSharedPointer<Entry> internalEntry; QSharedPointer<Macro> internalMacro; QSharedPointer<Preamble> internalPreamble; QSharedPointer<Comment> internalComment; ElementEditor *p; ElementWidget *previousWidget; ReferenceWidget *referenceWidget; QPushButton *buttonCheckWithBibTeX; /// Settings management through a push button with menu KSharedConfigPtr config; QPushButton *buttonOptions; QAction *actionForceShowAllWidgets, *actionLimitKeyboardTabStops; public: typedef QVector<ElementWidget *> WidgetList; QSharedPointer<Element> element; HidingTabWidget *tab; WidgetList widgets; SourceWidget *sourceWidget; FilesWidget *filesWidget; bool elementChanged, elementUnapplied; ElementEditorPrivate(bool scrollable, ElementEditor *parent) : file(nullptr), p(parent), previousWidget(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), elementChanged(false), elementUnapplied(false) { internalEntry = QSharedPointer<Entry>(); internalMacro = QSharedPointer<Macro>(); internalComment = QSharedPointer<Comment>(); internalPreamble = QSharedPointer<Preamble>(); createGUI(scrollable); } ~ElementEditorPrivate() override { clearWidgets(); } void clearWidgets() { for (int i = widgets.count() - 1; i >= 0; --i) { QWidget *w = widgets[i]; w->deleteLater(); } widgets.clear(); } void setElement(QSharedPointer<Element> element, const File *file) { this->element = element; this->file = file; referenceWidget->setOriginalElement(element); updateTabVisibility(); } void addTabWidgets() { for (const auto &etl : EntryLayout::instance()) { EntryConfiguredWidget *widget = new EntryConfiguredWidget(etl, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); connect(widget, &EntryConfiguredWidget::requestingTabChange, p, &ElementEditor::switchToTab); widgets << widget; if (previousWidget == nullptr) previousWidget = widget; ///< memorize the first tab int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); } ElementWidget *widget = new PreambleWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); widget = new MacroWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); filesWidget = new FilesWidget(tab); connect(filesWidget, &FilesWidget::modified, p, &ElementEditor::childModified); widgets << filesWidget; index = tab->addTab(filesWidget, filesWidget->icon(), filesWidget->label()); tab->hideTab(index); QStringList blacklistedFields; /// blacklist fields covered by EntryConfiguredWidget for (const auto &etl : EntryLayout::instance()) for (const auto &sfl : const_cast<const QList<SingleFieldLayout> &>(etl->singleFieldLayouts)) blacklistedFields << sfl.bibtexLabel; /// blacklist fields covered by FilesWidget blacklistedFields << QString(Entry::ftUrl) << QString(Entry::ftLocalFile) << QString(Entry::ftFile) << QString(Entry::ftDOI) << QStringLiteral("ee") << QStringLiteral("biburl") << QStringLiteral("postscript"); for (int i = 2; i < 256; ++i) // FIXME replace number by constant blacklistedFields << QString(Entry::ftUrl) + QString::number(i) << QString(Entry::ftLocalFile) + QString::number(i) << QString(Entry::ftFile) + QString::number(i) << QString(Entry::ftDOI) + QString::number(i) << QStringLiteral("ee") + QString::number(i) << QStringLiteral("postscript") + QString::number(i); widget = new OtherFieldsWidget(blacklistedFields, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); sourceWidget = new SourceWidget(tab); connect(sourceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << sourceWidget; index = tab->addTab(sourceWidget, sourceWidget->icon(), sourceWidget->label()); tab->hideTab(index); } void createGUI(bool scrollable) { /// load configuration for options push button static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); const bool showAll = configGroup.readEntry(keyEnableAllWidgets, true); const bool limitKeyboardTabStops = configGroup.readEntry(MenuLineEdit::keyLimitKeyboardTabStops, false); QBoxLayout *vLayout = new QVBoxLayout(p); referenceWidget = new ReferenceWidget(p); referenceWidget->setApplyElementInterface(this); connect(referenceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); connect(referenceWidget, &ReferenceWidget::entryTypeChanged, p, [this]() { updateReqOptWidgets(); }); vLayout->addWidget(referenceWidget, 0); widgets << referenceWidget; if (scrollable) { QScrollArea *sa = new QScrollArea(p); tab = new HidingTabWidget(sa); sa->setFrameStyle(0); sa->setWidget(tab); sa->setWidgetResizable(true); sa->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); sa->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); vLayout->addWidget(sa, 10); } else { tab = new HidingTabWidget(p); vLayout->addWidget(tab, 10); } QBoxLayout *hLayout = new QHBoxLayout(); vLayout->addLayout(hLayout, 0); /// Push button with menu to toggle various options buttonOptions = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), p); hLayout->addWidget(buttonOptions, 0); QMenu *menuOptions = new QMenu(buttonOptions); buttonOptions->setMenu(menuOptions); /// Option to show all fields or only those require for current entry type actionForceShowAllWidgets = menuOptions->addAction(i18n("Show all fields"), p, SLOT(updateReqOptWidgets())); actionForceShowAllWidgets->setCheckable(true); actionForceShowAllWidgets->setChecked(showAll); /// Option to disable tab key focus to reach/visit various non-editable widgets actionLimitKeyboardTabStops = menuOptions->addAction(i18n("Tab key visits only editable fields"), p, SLOT(limitKeyboardTabStops())); actionLimitKeyboardTabStops->setCheckable(true); actionLimitKeyboardTabStops->setChecked(limitKeyboardTabStops); hLayout->addStretch(10); buttonCheckWithBibTeX = new QPushButton(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check with BibTeX"), p); hLayout->addWidget(buttonCheckWithBibTeX, 0); connect(buttonCheckWithBibTeX, &QPushButton::clicked, p, [this]() { checkBibTeX(); }); addTabWidgets(); } void updateTabVisibility() { const QSignalBlocker blocker(tab); if (element.isNull()) { p->setEnabled(false); } else { p->setEnabled(true); int firstEnabledTab = 1024; for (ElementWidget *widget : const_cast<const WidgetList &>(widgets)) { const int index = tab->indexOf(widget); const bool canEdit = widget->canEdit(element.data()); if (widget == referenceWidget) { /// Reference widget widget->setVisible(canEdit); widget->setEnabled(canEdit); } else { if (canEdit) tab->showTab(widget); else if (index >= 0) tab->hideTab(index); if (canEdit && index >= 0 && index < firstEnabledTab) firstEnabledTab = index; } } if (firstEnabledTab < 1024) tab->setCurrentIndex(firstEnabledTab); } } /** * If this element editor makes use of a reference widget * (e.g. where entry type and entry id/macro key can be edited), * then return the current value of the entry id/macro key * editing widget. * Otherwise, return an empty string. * * @return Current value of entry id/macro key if any, otherwise empty string */ QString currentId() const { if (referenceWidget != nullptr) return referenceWidget->currentId(); return QString(); } void setCurrentId(const QString &newId) { if (referenceWidget != nullptr) return referenceWidget->setCurrentId(newId); } /** * Return the current File object set for this element editor. * May be NULL if nothing has been set or if it has been cleared. * * @return Current File object, may be nullptr */ const File *currentFile() const { return file; } void apply() { elementChanged = true; elementUnapplied = false; apply(element); } void apply(QSharedPointer<Element> element) override { QSharedPointer<Entry> e = element.dynamicCast<Entry>(); QSharedPointer<Macro> m = e.isNull() ? element.dynamicCast<Macro>() : QSharedPointer<Macro>(); QSharedPointer<Comment> c = e.isNull() && m.isNull() ? element.dynamicCast<Comment>() : QSharedPointer<Comment>(); QSharedPointer<Preamble> p = e.isNull() && m.isNull() && c.isNull() ? element.dynamicCast<Preamble>() : QSharedPointer<Preamble>(); if (tab->currentWidget() == sourceWidget) { /// Very simple if source view is active: BibTeX code contains /// all necessary data if (!e.isNull()) - sourceWidget->setElementClass(SourceWidget::elementEntry); + sourceWidget->setElementClass(SourceWidget::ElementClass::Entry); else if (!m.isNull()) - sourceWidget->setElementClass(SourceWidget::elementMacro); + sourceWidget->setElementClass(SourceWidget::ElementClass::Macro); else if (!p.isNull()) - sourceWidget->setElementClass(SourceWidget::elementPreamble); + sourceWidget->setElementClass(SourceWidget::ElementClass::Preamble); else - sourceWidget->setElementClass(SourceWidget::elementInvalid); + sourceWidget->setElementClass(SourceWidget::ElementClass::Invalid); sourceWidget->apply(element); } else { /// Start by assigning the current internal element's /// data to the output element if (!e.isNull()) *e = *internalEntry; else { if (!m.isNull()) *m = *internalMacro; else { if (!c.isNull()) *c = *internalComment; else { if (!p.isNull()) *p = *internalPreamble; else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::apply(QSharedPointer<Element> element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } /// The internal element may be outdated (only updated on tab switch), /// so apply the reference widget's data on the output element if (referenceWidget != nullptr) referenceWidget->apply(element); /// The internal element may be outdated (only updated on tab switch), /// so apply the current widget's data on the output element ElementWidget *currentElementWidget = qobject_cast<ElementWidget *>(tab->currentWidget()); if (currentElementWidget != nullptr) currentElementWidget->apply(element); } } bool validate(QWidget **widgetWithIssue, QString &message) const override { if (tab->currentWidget() == sourceWidget) { /// Source widget must check its textual content for being valid BibTeX code return sourceWidget->validate(widgetWithIssue, message); } else { /// All widgets except for the source widget must validate their values for (WidgetList::ConstIterator it = widgets.begin(); it != widgets.end(); ++it) { if ((*it) == sourceWidget) continue; const bool v = (*it)->validate(widgetWithIssue, message); /// A single widget failing to validate lets the whole validation fail if (!v) return false; } return true; } } void reset() { elementChanged = false; elementUnapplied = false; reset(element); /// show checkbox to enable all fields only if editing an entry actionForceShowAllWidgets->setVisible(!internalEntry.isNull()); /// Disable widgets if necessary if (!actionForceShowAllWidgets->isChecked()) updateReqOptWidgets(); } void reset(QSharedPointer<const Element> element) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) { (*it)->setFile(file); (*it)->reset(element); (*it)->setModified(false); } QSharedPointer<const Entry> e = element.dynamicCast<const Entry>(); if (!e.isNull()) { internalEntry = QSharedPointer<Entry>(new Entry(*e.data())); - sourceWidget->setElementClass(SourceWidget::elementEntry); + sourceWidget->setElementClass(SourceWidget::ElementClass::Entry); } else { QSharedPointer<const Macro> m = element.dynamicCast<const Macro>(); if (!m.isNull()) { internalMacro = QSharedPointer<Macro>(new Macro(*m.data())); - sourceWidget->setElementClass(SourceWidget::elementMacro); + sourceWidget->setElementClass(SourceWidget::ElementClass::Macro); } else { QSharedPointer<const Comment> c = element.dynamicCast<const Comment>(); if (!c.isNull()) { internalComment = QSharedPointer<Comment>(new Comment(*c.data())); - sourceWidget->setElementClass(SourceWidget::elementComment); + sourceWidget->setElementClass(SourceWidget::ElementClass::Comment); } else { QSharedPointer<const Preamble> p = element.dynamicCast<const Preamble>(); if (!p.isNull()) { internalPreamble = QSharedPointer<Preamble>(new Preamble(*p.data())); - sourceWidget->setElementClass(SourceWidget::elementPreamble); + sourceWidget->setElementClass(SourceWidget::ElementClass::Preamble); } else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::reset(QSharedPointer<const Element> element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } buttonCheckWithBibTeX->setEnabled(!internalEntry.isNull()); } void setReadOnly(bool isReadOnly) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setReadOnly(isReadOnly); } void updateReqOptWidgets() { /// this function is only relevant if editing an entry (and not e.g. a comment) if (internalEntry.isNull()) return; /// quick-and-dirty test if editing an entry /// make a temporary snapshot of the current state QSharedPointer<Entry> tempEntry = QSharedPointer<Entry>(new Entry()); apply(tempEntry); /// update the enabled/disabled state of required and optional widgets/fields bool forceVisible = actionForceShowAllWidgets->isChecked(); for (ElementWidget *elementWidget : const_cast<const WidgetList &>(widgets)) { elementWidget->showReqOptWidgets(forceVisible, tempEntry->type()); } /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(keyEnableAllWidgets, actionForceShowAllWidgets->isChecked()); config->sync(); } void limitKeyboardTabStops() { /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(MenuLineEdit::keyLimitKeyboardTabStops, actionLimitKeyboardTabStops->isChecked()); config->sync(); /// notify all listening MenuLineEdit widgets to change their behavior NotificationHub::publishEvent(MenuLineEdit::MenuLineConfigurationChangedEvent); } void switchTo(QWidget *futureTab) { /// Switched from source widget to another widget? const bool isToSourceWidget = futureTab == sourceWidget; /// Switch from some widget to the source widget? const bool isFromSourceWidget = previousWidget == sourceWidget; /// Interprete future widget as an ElementWidget ElementWidget *futureWidget = qobject_cast<ElementWidget *>(futureTab); /// Past and future ElementWidget values are valid? if (previousWidget != nullptr && futureWidget != nullptr) { /// Assign to temp wihch internal variable holds current state QSharedPointer<Element> temp; if (!internalEntry.isNull()) temp = internalEntry; else if (!internalMacro.isNull()) temp = internalMacro; else if (!internalComment.isNull()) temp = internalComment; else if (!internalPreamble.isNull()) temp = internalPreamble; Q_ASSERT_X(!temp.isNull(), "void ElementEditor::ElementEditorPrivate::switchTo(QWidget *newTab)", "temp is NULL"); /// Past widget writes its state to the internal state previousWidget->apply(temp); /// Before switching to source widget, store internally reference widget's state if (isToSourceWidget && referenceWidget != nullptr) referenceWidget->apply(temp); /// Tell future widget to initialize itself based on internal state futureWidget->reset(temp); /// When switchin from source widget to another widget, initialize reference widget if (isFromSourceWidget && referenceWidget != nullptr) referenceWidget->reset(temp); } previousWidget = futureWidget; /// Enable/disable tabs for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setEnabled(!isToSourceWidget || *it == futureTab); } /** * Test current entry if it compiles with BibTeX. * Show warnings and errors in message box. */ void checkBibTeX() { /// disable GUI under process p->setEnabled(false); QSharedPointer<Entry> entry = QSharedPointer<Entry>(new Entry()); apply(entry); CheckBibTeX::checkBibTeX(entry, file, p); p->setEnabled(true); } void setModified(bool newIsModified) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setModified(newIsModified); } void referenceWidgetSetEntryIdByDefault() { referenceWidget->setEntryIdByDefault(); } }; ElementEditor::ElementEditor(bool scrollable, QWidget *parent) : QWidget(parent), d(new ElementEditorPrivate(scrollable, this)) { connect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); } ElementEditor::~ElementEditor() { disconnect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); delete d; } void ElementEditor::apply() { /// The prime problem to tackle in this function is to cope with /// invalid/problematic entry ids or macro keys, respectively: /// - empty ids/keys /// - ids/keys that are duplicates of already used ids/keys QSharedPointer<Entry> entry = d->element.dynamicCast<Entry>(); QSharedPointer<Macro> macro = d->element.dynamicCast<Macro>(); /// Only for entry or macro bother with duplicate ids (but not for preamble or comment) if (!entry.isNull() || !macro.isNull()) { /// Determine id/key as it was set before the current editing started const QString originalId = !entry.isNull() ? entry->id() : (!macro.isNull() ? macro->key() : QString()); /// Get the id/key as it is in the editing widget right now const QString newId = d->currentId(); /// Keep track whether the 'original' id/key or the 'new' id/key will eventually be used enum IdToUse {UseOriginalId, UseNewId}; IdToUse idToUse = UseNewId; if (newId.isEmpty() && !originalId.isEmpty()) { /// New id/key is empty (invalid by definition), so just notify use and revert back to original id/key /// (assuming that original id/key is valid) KMessageBox::sorry(this, i18n("No id was entered, so the previous id '%1' will be restored.", originalId), i18n("No id given")); idToUse = UseOriginalId; } else if (!newId.isEmpty()) { // FIXME test if !originalId.isEmpty() ? /// If new id/key is not empty, then check if it is identical to another entry/macro in the current file const QSharedPointer<Element> knownElementWithSameId = d->currentFile() != nullptr ? d->currentFile()->containsKey(newId) : QSharedPointer<Element>(); if (!knownElementWithSameId.isNull() && d->element != knownElementWithSameId) { /// Some other, different element (entry or macro) uses same id/key, so ask user how to proceed const int msgBoxResult = KMessageBox::warningContinueCancel(this, i18n("The entered id '%1' is already in use for another element.\n\nKeep original id '%2' instead?", newId, originalId), i18n("Id already in use"), KGuiItem(i18n("Keep duplicate ids")), KGuiItem(i18n("Restore original id"))); idToUse = msgBoxResult == KMessageBox::Continue ? UseNewId : UseOriginalId; } } if (idToUse == UseOriginalId) { /// As 'apply()' above set the 'new' id/key but the 'original' id/key is to be used, /// now UI must be updated accordingly. Changes will propagate to the entry id or /// macro key, respectively, when invoking apply() further down d->setCurrentId(originalId); } /// Case idToUse == UseNewId does not need to get handled as newId == d->currentId() } d->apply(); d->setModified(false); emit modified(false); } void ElementEditor::reset() { d->reset(); emit modified(false); } bool ElementEditor::validate() { QWidget *widgetWithIssue = nullptr; QString message; if (!validate(&widgetWithIssue, message)) { const QString msgBoxMessage = message.isEmpty() ? i18n("Validation for the current element failed.") : i18n("Validation for the current element failed:\n%1", message); KMessageBox::error(this, msgBoxMessage, i18n("Element validation failed")); if (widgetWithIssue != nullptr) { /// Probe if widget with issue is inside a QTabWiget; if yes, make parenting tab the current tab QWidget *cur = widgetWithIssue; do { QTabWidget *tabWidget = cur->parent() != nullptr && cur->parent()->parent() != nullptr ? qobject_cast<QTabWidget *>(cur->parent()->parent()) : nullptr; if (tabWidget != nullptr) { tabWidget->setCurrentWidget(cur); break; } cur = qobject_cast<QWidget *>(cur->parent()); } while (cur != nullptr); /// Set focus to widget with issue widgetWithIssue->setFocus(); } return false; } return true; } void ElementEditor::setElement(QSharedPointer<Element> element, const File *file) { d->setElement(element, file); d->reset(); emit modified(false); } void ElementEditor::setElement(QSharedPointer<const Element> element, const File *file) { QSharedPointer<Element> clone; QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) clone = QSharedPointer<Entry>(new Entry(*entry.data())); else { QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) clone = QSharedPointer<Macro>(new Macro(*macro.data())); else { QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) clone = QSharedPointer<Preamble>(new Preamble(*preamble.data())); else { QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull()) clone = QSharedPointer<Comment>(new Comment(*comment.data())); else Q_ASSERT_X(element == nullptr, "ElementEditor::ElementEditor(const Element *element, QWidget *parent)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } d->setElement(clone, file); d->reset(); } void ElementEditor::setReadOnly(bool isReadOnly) { d->setReadOnly(isReadOnly); } bool ElementEditor::elementChanged() { return d->elementChanged; } bool ElementEditor::elementUnapplied() { return d->elementUnapplied; } bool ElementEditor::validate(QWidget **widgetWithIssue, QString &message) { return d->validate(widgetWithIssue, message); } QWidget *ElementEditor::currentPage() const { return d->tab->currentWidget(); } void ElementEditor::setCurrentPage(QWidget *page) { if (d->tab->indexOf(page) >= 0) d->tab->setCurrentWidget(page); } void ElementEditor::tabChanged() { d->switchTo(d->tab->currentWidget()); } void ElementEditor::switchToTab(const QString &tabIdentifier) { if (tabIdentifier == QStringLiteral("source")) setCurrentPage(d->sourceWidget); else if (tabIdentifier == QStringLiteral("external")) setCurrentPage(d->filesWidget); else { for (ElementWidget *widget : d->widgets) { EntryConfiguredWidget *ecw = qobject_cast<EntryConfiguredWidget *>(widget); if (ecw != nullptr && ecw->identifier() == tabIdentifier) { setCurrentPage(ecw); break; } } } } void ElementEditor::childModified(bool m) { if (m) { d->elementUnapplied = true; d->referenceWidgetSetEntryIdByDefault(); } emit modified(m); } diff --git a/src/gui/element/elementwidgets.cpp b/src/gui/element/elementwidgets.cpp index f2dadee6..d3d6a9ac 100644 --- a/src/gui/element/elementwidgets.cpp +++ b/src/gui/element/elementwidgets.cpp @@ -1,1391 +1,1396 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "elementwidgets.h" #include <QLayout> #include <QBuffer> #include <QLabel> #include <QLineEdit> #include <QComboBox> #include <QTreeWidget> #include <QFileInfo> #include <QDropEvent> #include <QMenu> #include <QMimeType> #include <QMimeData> #include <QSortFilterProxyModel> #include <QStyle> #include <QPushButton> #include <QFontDatabase> #include <QRegularExpression> #include <KLocalizedString> #include <KRun> #include <KTextEditor/Document> #include <KTextEditor/Editor> #include <KTextEditor/View> #include <KMessageWidget> #include <kio_version.h> #include <KBibTeX> #include <Preferences> #include <BibTeXEntries> #include <BibTeXFields> #include <Entry> #include <Macro> #include <Preamble> #include <File> #include <FileInfo> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <FileExporterBibTeX2HTML> #include <IdSuggestions> #include "field/fieldinput.h" #include "field/fieldlineedit.h" #include "delayedexecutiontimer.h" #include "logging_gui.h" static const unsigned int interColumnSpace = 16; static const char *PropertyIdSuggestion = "PropertyIdSuggestion"; ElementWidget::ElementWidget(QWidget *parent) : QWidget(parent), isReadOnly(false), m_file(nullptr), m_isModified(false) { /// nothing } bool ElementWidget::isModified() const { return m_isModified; } void ElementWidget::setModified(bool newIsModified) { m_isModified = newIsModified; emit modified(newIsModified); } void ElementWidget::gotModified() { setModified(true); } EntryConfiguredWidget::EntryConfiguredWidget(const QSharedPointer<const EntryTabLayout> &entryTabLayout, QWidget *parent) : ElementWidget(parent), fieldInputCount(entryTabLayout->singleFieldLayouts.size()), numCols(entryTabLayout->columns), etl(entryTabLayout) { vboxLayout = new QVBoxLayout(this); gridLayout = new QGridLayout(); vboxLayout->addLayout(gridLayout, 100); /// Initialize list of field input widgets plus labels listOfLabeledFieldInput = new LabeledFieldInput*[fieldInputCount]; createGUI(); } EntryConfiguredWidget::~EntryConfiguredWidget() { delete[] listOfLabeledFieldInput; } bool EntryConfiguredWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (entry.isNull()) return false; for (QMap<QString, FieldInput *>::ConstIterator it = bibtexKeyToWidget.constBegin(); it != bibtexKeyToWidget.constEnd(); ++it) { Value value; it.value()->apply(value); entry->remove(it.key()); if (!value.isEmpty()) entry->insert(it.key(), value); } return true; } bool EntryConfiguredWidget::reset(QSharedPointer<const Element> element) { QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (entry.isNull()) return false; /// clear all widgets for (QMap<QString, FieldInput *>::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(m_file); it.value()->clear(); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { const QString key = it.key().toLower(); if (bibtexKeyToWidget.contains(key)) { FieldInput *fieldInput = bibtexKeyToWidget[key]; fieldInput->setElement(element.data()); fieldInput->reset(it.value()); } } return true; } bool EntryConfiguredWidget::validate(QWidget **widgetWithIssue, QString &message) const { for (int i = fieldInputCount - 1; i >= 0; --i) { const bool v = listOfLabeledFieldInput[i]->fieldInput->validate(widgetWithIssue, message); if (!v) return false; } return true; } void EntryConfiguredWidget::showReqOptWidgets(bool forceVisible, const QString &entryType) { layoutGUI(forceVisible, entryType); } QString EntryConfiguredWidget::identifier() const { return etl->identifier; } void EntryConfiguredWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); for (QMap<QString, FieldInput *>::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) it.value()->setReadOnly(_isReadOnly); } QString EntryConfiguredWidget::label() { return etl->uiCaption; } QIcon EntryConfiguredWidget::icon() { return QIcon::fromTheme(etl->iconName); } void EntryConfiguredWidget::setFile(const File *file) { for (QMap<QString, FieldInput *>::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(file); if (file != nullptr) { /// list of unique values for same field QStringList list = file->uniqueEntryValuesList(it.key()); /// for crossref fields, add all entries' ids if (it.key().toLower() == Entry::ftCrossRef) - list.append(file->allKeys(File::etEntry)); + list.append(file->allKeys(File::ElementType::Entry)); /// add macro keys - list.append(file->allKeys(File::etMacro)); + list.append(file->allKeys(File::ElementType::Macro)); it.value()->setCompletionItems(list); } } ElementWidget::setFile(file); } bool EntryConfiguredWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void EntryConfiguredWidget::infoMessageLinkActivated(const QString &contents) { if (contents.startsWith(QStringLiteral("#tab:"))) { const QString tabIdentifier = contents.mid(5); emit requestingTabChange(tabIdentifier); } } void EntryConfiguredWidget::createGUI() { int i = 0; for (const SingleFieldLayout &sfl : const_cast<const QList<SingleFieldLayout> &>(etl->singleFieldLayouts)) { LabeledFieldInput *labeledFieldInput = new LabeledFieldInput; /// create an editing widget for this field const FieldDescription &fd = BibTeXFields::instance().find(sfl.bibtexLabel); labeledFieldInput->fieldInput = new FieldInput(sfl.fieldInputLayout, fd.preferredTypeFlag, fd.typeFlags, this); labeledFieldInput->fieldInput->setFieldKey(sfl.bibtexLabel); bibtexKeyToWidget.insert(sfl.bibtexLabel, labeledFieldInput->fieldInput); connect(labeledFieldInput->fieldInput, &FieldInput::modified, this, &EntryConfiguredWidget::gotModified); /// memorize if field input should grow vertically (e.g. is a list) - labeledFieldInput->isVerticallyMinimumExpaning = sfl.fieldInputLayout == KBibTeX::MultiLine || sfl.fieldInputLayout == KBibTeX::List || sfl.fieldInputLayout == KBibTeX::PersonList || sfl.fieldInputLayout == KBibTeX::KeywordList; + labeledFieldInput->isVerticallyMinimumExpaning = sfl.fieldInputLayout == KBibTeX::FieldInputType::MultiLine || sfl.fieldInputLayout == KBibTeX::FieldInputType::List || sfl.fieldInputLayout == KBibTeX::FieldInputType::PersonList || sfl.fieldInputLayout == KBibTeX::FieldInputType::KeywordList; /// create a label next to the editing widget labeledFieldInput->label = new QLabel(QString(QStringLiteral("%1:")).arg(sfl.uiLabel), this); labeledFieldInput->label->setBuddy(labeledFieldInput->fieldInput->buddy()); /// align label's text vertically to match field input const Qt::Alignment horizontalAlignment = static_cast<Qt::Alignment>(labeledFieldInput->label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)) & Qt::AlignHorizontal_Mask; labeledFieldInput->label->setAlignment(horizontalAlignment | (labeledFieldInput->isVerticallyMinimumExpaning ? Qt::AlignTop : Qt::AlignVCenter)); listOfLabeledFieldInput[i] = labeledFieldInput; ++i; } if (!etl->infoMessages.isEmpty()) for (const QString &infoMessage : etl->infoMessages) { KMessageWidget *infoMessagesWidget = new KMessageWidget(i18n(infoMessage.toUtf8().constData()), this); connect(infoMessagesWidget, &KMessageWidget::linkActivated, this, &EntryConfiguredWidget::infoMessageLinkActivated); vboxLayout->addWidget(infoMessagesWidget, 1); } layoutGUI(true); } void EntryConfiguredWidget::layoutGUI(bool forceVisible, const QString &entryType) { QStringList visibleItems; if (!forceVisible && !entryType.isEmpty()) { const QString entryTypeLc = entryType.toLower(); for (const auto &ed : BibTeXEntries::instance()) { if (entryTypeLc == ed.upperCamelCase.toLower() || entryTypeLc == ed.upperCamelCaseAlt.toLower()) { /// this ugly conversion is necessary because we have a "^" (xor) and "|" (and/or) /// syntax to differentiate required items (not used yet, but will be used /// later if missing required items are marked). QString visible = ed.requiredItems.join(QStringLiteral(",")); visible += QLatin1Char(',') + ed.optionalItems.join(QStringLiteral(",")); visible = visible.replace(QLatin1Char('|'), QLatin1Char(',')).replace(QLatin1Char('^'), QLatin1Char(',')); visibleItems = visible.split(QStringLiteral(",")); break; } } } else if (!forceVisible) { // TODO is this an error condition? } /// variables to keep track which and how many field inputs will be visible int countVisible = 0; QScopedArrayPointer<bool> visible(new bool[fieldInputCount]); /// ... and if any field input is vertically expaning /// (e.g. a list, important for layout) bool anyoneVerticallyExpanding = false; for (int i = fieldInputCount - 1; i >= 0; --i) { listOfLabeledFieldInput[i]->label->setVisible(false); listOfLabeledFieldInput[i]->fieldInput->setVisible(false); gridLayout->removeWidget(listOfLabeledFieldInput[i]->label); gridLayout->removeWidget(listOfLabeledFieldInput[i]->fieldInput); const QString key = bibtexKeyToWidget.key(listOfLabeledFieldInput[i]->fieldInput).toLower(); const FieldDescription &fd = BibTeXFields::instance().find(key); Value value; listOfLabeledFieldInput[i]->fieldInput->apply(value); /// Hide non-required and non-optional type-dependent fields, /// except if the field has content visible[i] = forceVisible || fd.typeIndependent || !value.isEmpty() || visibleItems.contains(key); if (visible[i]) { ++countVisible; anyoneVerticallyExpanding |= listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning; } } int numRows = countVisible / numCols; if (countVisible % numCols > 0) ++numRows; gridLayout->setRowStretch(numRows, anyoneVerticallyExpanding ? 0 : 1000); int col = 0, row = 0; for (int i = 0; i < fieldInputCount; ++i) if (visible[i]) { /// add label and field input to new position in grid layout gridLayout->addWidget(listOfLabeledFieldInput[i]->label, row, col * 3); gridLayout->addWidget(listOfLabeledFieldInput[i]->fieldInput, row, col * 3 + 1); /// set row stretch gridLayout->setRowStretch(row, listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning ? 1000 : 0); /// set column stretch and spacing gridLayout->setColumnStretch(col * 3, 1); gridLayout->setColumnStretch(col * 3 + 1, 1000); if (col > 0) gridLayout->setColumnMinimumWidth(col * 3 - 1, interColumnSpace); /// count rows and columns correctly ++row; if (row >= numRows) { row = 0; ++col; } /// finally, set label and field input visible again listOfLabeledFieldInput[i]->label->setVisible(true); listOfLabeledFieldInput[i]->fieldInput->setVisible(true); // FIXME expensive! } if (countVisible > 0) { /// fix row stretch for (int i = numRows + 1; i < 100; ++i) gridLayout->setRowStretch(i, 0); /// hide unused columns for (int i = (col + (row == 0 ? 0 : 1)) * 3 - 1; i < 100; ++i) { gridLayout->setColumnMinimumWidth(i, 0); gridLayout->setColumnStretch(i, 0); } } } ReferenceWidget::ReferenceWidget(QWidget *parent) : ElementWidget(parent), m_applyElement(nullptr), m_entryIdManuallySet(false), m_element(QSharedPointer<Element>()) { createGUI(); } bool ReferenceWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode bool result = false; QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { entry->setType(computeType()); entry->setId(entryId->text()); result = true; } else { QSharedPointer<Macro> macro = element.dynamicCast<Macro>(); if (!macro.isNull()) { macro->setKey(entryId->text()); result = true; } } return result; } bool ReferenceWidget::reset(QSharedPointer<const Element> element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widgets' values const QSignalBlocker blockerEntryTypeLineEdit(entryType->lineEdit()); const QSignalBlocker blockerEntryIdLineEdit(entryId); bool result = false; QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { entryType->setEnabled(!isReadOnly); buttonSuggestId->setEnabled(!isReadOnly); - QString type = BibTeXEntries::instance().format(entry->type(), KBibTeX::cUpperCamelCase); + QString type = BibTeXEntries::instance().format(entry->type(), KBibTeX::Casing::UpperCamelCase); int index = entryType->findData(type); if (index == -1) { const QString typeLower(type.toLower()); for (const auto &ed : BibTeXEntries::instance()) if (typeLower == ed.upperCamelCaseAlt.toLower()) { index = entryType->findData(ed.upperCamelCase); break; } } entryType->setCurrentIndex(index); if (index == -1) { /// A customized value not known to KBibTeX entryType->lineEdit()->setText(type); } entryId->setText(entry->id()); /// New entries have no values. Use this fact /// to recognize new entries, for which it is /// allowed to automatic set their ids /// if a default id suggestion had been specified. m_entryIdManuallySet = entry->count() > 0; result = true; } else { entryType->setEnabled(false); buttonSuggestId->setEnabled(false); QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) { entryType->lineEdit()->setText(i18n("Macro")); entryId->setText(macro->key()); result = true; } } return result; } bool ReferenceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); static const QRegularExpression validTypeRegExp(QStringLiteral("^[a-z]+$"), QRegularExpression::CaseInsensitiveOption); const QString type = computeType(); const QRegularExpressionMatch validTypeMatch = validTypeRegExp.match(type); if (!validTypeMatch.hasMatch() || validTypeMatch.capturedLength() != type.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryType; message = i18n("Element type '%1' is invalid.", type); return false; } static const QRegularExpression validIdRegExp(QStringLiteral("^[a-z0-9][a-z0-9_:.+/$\\\"&-]*$"), QRegularExpression::CaseInsensitiveOption); const QString id = entryId->text(); const QRegularExpressionMatch validIdMatch = validIdRegExp.match(id); if (!validIdMatch.hasMatch() || validIdMatch.capturedLength() != id.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryId; message = i18n("Id '%1' is invalid", id); return false; } return true; } void ReferenceWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); entryId->setReadOnly(_isReadOnly); entryType->setEnabled(!_isReadOnly); } QString ReferenceWidget::label() { return QString(); } QIcon ReferenceWidget::icon() { return QIcon(); } bool ReferenceWidget::canEdit(const Element *element) { return Entry::isEntry(*element) || Macro::isMacro(*element); } void ReferenceWidget::setOriginalElement(const QSharedPointer<Element> &orig) { m_element = orig; } QString ReferenceWidget::currentId() const { return entryId->text(); } void ReferenceWidget::setCurrentId(const QString &newId) { entryId->setText(newId); } void ReferenceWidget::createGUI() { QHBoxLayout *layout = new QHBoxLayout(this); entryType = new QComboBox(this); entryType->setEditable(true); entryType->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QLabel *label = new QLabel(i18n("Type:"), this); label->setBuddy(entryType); layout->addWidget(label); layout->addWidget(entryType); layout->addSpacing(interColumnSpace); entryId = new QLineEdit(this); entryId->setClearButtonEnabled(true); label = new QLabel(i18n("Id:"), this); label->setBuddy(entryId); layout->addWidget(label); layout->addWidget(entryId); for (const auto &ed : BibTeXEntries::instance()) entryType->addItem(ed.label, ed.upperCamelCase); /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(entryType); proxy->setSortLocaleAware(true); proxy->setSourceModel(entryType->model()); entryType->model()->setParent(proxy); entryType->setModel(proxy); entryType->model()->sort(0); /// Button with a menu listing a set of preconfigured id suggestions buttonSuggestId = new QPushButton(QIcon::fromTheme(QStringLiteral("view-filter")), QString(), this); buttonSuggestId->setToolTip(i18n("Select a suggested id for this entry")); layout->addWidget(buttonSuggestId); QMenu *suggestionsMenu = new QMenu(buttonSuggestId); buttonSuggestId->setMenu(suggestionsMenu); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::entryTypeChanged); connect(suggestionsMenu, &QMenu::aboutToShow, this, &ReferenceWidget::prepareSuggestionsMenu); } void ReferenceWidget::prepareSuggestionsMenu() { /// Collect information on the current entry as it is edited QSharedPointer<Entry> guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer<Entry> crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); static const IdSuggestions *idSuggestions = new IdSuggestions(); QMenu *suggestionsMenu = buttonSuggestId->menu(); suggestionsMenu->clear(); /// Keep track of shown suggestions to avoid duplicates QSet<QString> knownIdSuggestion; const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); const auto formatIdList = idSuggestions->formatIdList(*crossrefResolvedEntry.data()); for (const QString &suggestionBase : formatIdList) { bool isDefault = suggestionBase == defaultSuggestion; QString suggestion = suggestionBase; /// Test for duplicate ids, use fallback ids with numeric suffix if (m_file != nullptr && m_file->containsKey(suggestion)) { int suffix = 2; while (m_file->containsKey(suggestion = suggestionBase + QChar('_') + QString::number(suffix))) ++suffix; } /// Keep track of shown suggestions to avoid duplicates if (knownIdSuggestion.contains(suggestion)) continue; else knownIdSuggestion.insert(suggestion); /// Create action for suggestion, use icon depending if default or not QAction *suggestionAction = new QAction(suggestion, suggestionsMenu); suggestionAction->setIcon(QIcon::fromTheme(isDefault ? QStringLiteral("favorites") : QStringLiteral("view-filter"))); /// Mesh action into GUI suggestionsMenu->addAction(suggestionAction); connect(suggestionAction, &QAction::triggered, this, &ReferenceWidget::insertSuggestionFromAction); /// Remember suggestion string for time when action gets triggered suggestionAction->setProperty(PropertyIdSuggestion, suggestion); } } void ReferenceWidget::insertSuggestionFromAction() { QAction *action = qobject_cast<QAction *>(sender()); if (action != nullptr) { const QString suggestion = action->property(PropertyIdSuggestion).toString(); entryId->setText(suggestion); } } void ReferenceWidget::entryIdManuallyChanged() { m_entryIdManuallySet = true; gotModified(); } void ReferenceWidget::setEntryIdByDefault() { if (isReadOnly) { /// Never set the suggestion automatically if in read-only mode return; } if (m_entryIdManuallySet) { /// If user changed entry id manually, /// do not overwrite it by a default value return; } static const IdSuggestions *idSuggestions = new IdSuggestions(); /// If there is a default suggestion format set ... if (idSuggestions->hasDefaultFormat()) { /// Collect information on the current entry as it is edited QSharedPointer<Entry> guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer<Entry> crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); /// Determine default suggestion based on current data const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); if (!defaultSuggestion.isEmpty()) { const QSignalBlocker blocker(entryId); /// Apply default suggestion to widget entryId->setText(defaultSuggestion); } } } QString ReferenceWidget::computeType() const { if (entryType->currentIndex() < 0 || entryType->lineEdit()->isModified()) - return BibTeXEntries::instance().format(entryType->lineEdit()->text(), KBibTeX::cUpperCamelCase); + return BibTeXEntries::instance().format(entryType->lineEdit()->text(), KBibTeX::Casing::UpperCamelCase); else return entryType->itemData(entryType->currentIndex()).toString(); } FilesWidget::FilesWidget(QWidget *parent) : ElementWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); - fileList = new FieldInput(KBibTeX::UrlList, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, this); + fileList = new FieldInput(KBibTeX::FieldInputType::UrlList, KBibTeX::TypeFlag::Verbatim /* eventually ignored, see constructor of UrlListEdit */, KBibTeX::TypeFlag::Verbatim /* eventually ignored, see constructor of UrlListEdit */, this); fileList->setFieldKey(QStringLiteral("^external")); layout->addWidget(fileList); connect(fileList, &FieldInput::modified, this, &FilesWidget::gotModified); } bool FilesWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (entry.isNull()) return false; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; entry->remove(key); } Value combinedValue; fileList->apply(combinedValue); Value urlValue, doiValue, localFileValue; urlValue.reserve(combinedValue.size()); doiValue.reserve(combinedValue.size()); localFileValue.reserve(combinedValue.size()); for (const auto &valueItem : const_cast<const Value &>(combinedValue)) { const QSharedPointer<const VerbatimText> verbatimText = valueItem.dynamicCast<const VerbatimText>(); if (!verbatimText.isNull()) { const QString text = verbatimText->text(); QRegularExpressionMatch match; if ((match = KBibTeX::urlRegExp.match(text)).hasMatch()) { /// add full URL VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (urlValue.contains(*newVT)) delete newVT; else urlValue.append(QSharedPointer<VerbatimText>(newVT)); } else if ((match = KBibTeX::doiRegExp.match(text)).hasMatch()) { /// add DOI VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (doiValue.contains(*newVT)) delete newVT; else doiValue.append(QSharedPointer<VerbatimText>(newVT)); } else { /// add anything else (e.g. local file) VerbatimText *newVT = new VerbatimText(*verbatimText); /// test for duplicates if (localFileValue.contains(*newVT)) delete newVT; else localFileValue.append(QSharedPointer<VerbatimText>(newVT)); } } } if (urlValue.isEmpty()) entry->remove(Entry::ftUrl); else entry->insert(Entry::ftUrl, urlValue); if (localFileValue.isEmpty()) { entry->remove(Entry::ftFile); entry->remove(Entry::ftLocalFile); - } else if (Preferences::instance().bibliographySystem() == Preferences::BibLaTeX) { + } else if (Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibLaTeX) { entry->remove(Entry::ftLocalFile); entry->insert(Entry::ftFile, localFileValue); - } else if (Preferences::instance().bibliographySystem() == Preferences::BibTeX) { + } else if (Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibTeX) { entry->remove(Entry::ftFile); entry->insert(Entry::ftLocalFile, localFileValue); } if (doiValue.isEmpty()) entry->remove(Entry::ftDOI); else entry->insert(Entry::ftDOI, doiValue); return true; } bool FilesWidget::reset(QSharedPointer<const Element> element) { QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (entry.isNull()) return false; Value combinedValue; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; const Value &value = entry->operator [](key); for (const auto &valueItem : const_cast<const Value &>(value)) combinedValue.append(valueItem); } fileList->setElement(element.data()); fileList->reset(combinedValue); return true; } bool FilesWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fileList->validate(widgetWithIssue, message); } void FilesWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fileList->setReadOnly(_isReadOnly); } QString FilesWidget::label() { return i18n("External"); } QIcon FilesWidget::icon() { return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); } void FilesWidget::setFile(const File *file) { ElementWidget::setFile(file); fileList->setFile(file); } bool FilesWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } const QStringList FilesWidget::keyStart {Entry::ftUrl, QStringLiteral("postscript"), Entry::ftLocalFile, Entry::ftDOI, Entry::ftFile, QStringLiteral("ee"), QStringLiteral("biburl")}; OtherFieldsWidget::OtherFieldsWidget(const QStringList &blacklistedFields, QWidget *parent) : ElementWidget(parent), blackListed(blacklistedFields) { internalEntry = QSharedPointer<Entry>(new Entry()); createGUI(); } OtherFieldsWidget::~OtherFieldsWidget() { delete fieldContent; } bool OtherFieldsWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (entry.isNull()) return false; for (const QString &key : const_cast<const QStringList &>(deletedKeys)) entry->remove(key); for (const QString &key : const_cast<const QStringList &>(modifiedKeys)) { entry->remove(key); entry->insert(key, internalEntry->value(key)); } return true; } bool OtherFieldsWidget::reset(QSharedPointer<const Element> element) { QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (entry.isNull()) return false; internalEntry = QSharedPointer<Entry>(new Entry(*entry.data())); deletedKeys.clear(); // FIXME clearing list may be premature here... modifiedKeys.clear(); // FIXME clearing list may be premature here... updateList(); updateGUI(); return true; } bool OtherFieldsWidget::validate(QWidget **, QString &) const { /// No checks to make here; all actual check will be conducted in actionAddApply(..) return true; } void OtherFieldsWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldName->setReadOnly(_isReadOnly); fieldContent->setReadOnly(_isReadOnly); /// will take care of enabled/disabling buttons updateGUI(); updateList(); } QString OtherFieldsWidget::label() { return i18n("Other Fields"); } QIcon OtherFieldsWidget::icon() { return QIcon::fromTheme(QStringLiteral("other")); } bool OtherFieldsWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void OtherFieldsWidget::listElementExecuted(QTreeWidgetItem *item, int column) { Q_UNUSED(column) /// we do not care which column got clicked QString key = item->text(0); fieldName->setText(key); fieldContent->reset(internalEntry->value(key)); } void OtherFieldsWidget::listCurrentChanged(QTreeWidgetItem *item, QTreeWidgetItem *previous) { Q_UNUSED(previous) bool validUrl = false; bool somethingSelected = item != nullptr; buttonDelete->setEnabled(somethingSelected && !isReadOnly); if (somethingSelected) { currentUrl = QUrl(item->text(1)); validUrl = currentUrl.isValid() && currentUrl.isLocalFile() & QFileInfo::exists(currentUrl.toLocalFile()); if (!validUrl) { const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(item->text(1)); if (urlRegExpMatch.hasMatch()) { currentUrl = QUrl(urlRegExpMatch.captured(0)); validUrl = currentUrl.isValid(); buttonOpen->setEnabled(validUrl); } } } if (!validUrl) currentUrl = QUrl(); buttonOpen->setEnabled(validUrl); } void OtherFieldsWidget::actionAddApply() { if (isReadOnly) return; /// never modify anything if in read-only mode QString key = fieldName->text(), message; Value value; if (!fieldContent->validate(nullptr, message)) return; ///< invalid values should not get applied if (!fieldContent->apply(value)) return; if (internalEntry->contains(key)) internalEntry->remove(key); internalEntry->insert(key, value); if (!modifiedKeys.contains(key)) modifiedKeys << key; updateList(); updateGUI(); gotModified(); } void OtherFieldsWidget::actionDelete() { if (isReadOnly) return; /// never modify anything if in read-only mode Q_ASSERT_X(otherFieldsList->currentItem() != nullptr, "OtherFieldsWidget::actionDelete", "otherFieldsList->currentItem() is NULL"); QString key = otherFieldsList->currentItem()->text(0); if (!deletedKeys.contains(key)) deletedKeys << key; internalEntry->remove(key); updateList(); updateGUI(); listCurrentChanged(otherFieldsList->currentItem(), nullptr); gotModified(); } void OtherFieldsWidget::actionOpen() { if (currentUrl.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(currentUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(currentUrl, mimeTypeName, this, KRun::RunFlags()); } } void OtherFieldsWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); /// set row and column stretches based on chosen layout layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); layout->setRowStretch(0, 0); layout->setRowStretch(1, 1); layout->setRowStretch(2, 0); layout->setRowStretch(3, 0); layout->setRowStretch(4, 1); QLabel *label = new QLabel(i18n("Name:"), this); layout->addWidget(label, 0, 0, 1, 1); label->setAlignment(static_cast<Qt::Alignment>(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldName = new QLineEdit(this); layout->addWidget(fieldName, 0, 1, 1, 1); label->setBuddy(fieldName); buttonAddApply = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), this); buttonAddApply->setEnabled(false); layout->addWidget(buttonAddApply, 0, 2, 1, 1); label = new QLabel(i18n("Content:"), this); layout->addWidget(label, 1, 0, 1, 1); label->setAlignment(static_cast<Qt::Alignment>(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); - fieldContent = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); + fieldContent = new FieldInput(KBibTeX::FieldInputType::MultiLine, KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, this); layout->addWidget(fieldContent, 1, 1, 1, 2); label->setBuddy(fieldContent->buddy()); label = new QLabel(i18n("List:"), this); layout->addWidget(label, 2, 0, 1, 1); label->setAlignment(static_cast<Qt::Alignment>(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); otherFieldsList = new QTreeWidget(this); otherFieldsList->setHeaderLabels(QStringList {i18n("Key"), i18n("Value")}); otherFieldsList->setRootIsDecorated(false); layout->addWidget(otherFieldsList, 2, 1, 3, 1); label->setBuddy(otherFieldsList); buttonDelete = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete"), this); buttonDelete->setEnabled(false); layout->addWidget(buttonDelete, 2, 2, 1, 1); buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), this); buttonOpen->setEnabled(false); layout->addWidget(buttonOpen, 3, 2, 1, 1); connect(otherFieldsList, &QTreeWidget::itemActivated, this, &OtherFieldsWidget::listElementExecuted); connect(otherFieldsList, &QTreeWidget::currentItemChanged, this, &OtherFieldsWidget::listCurrentChanged); connect(otherFieldsList, &QTreeWidget::itemSelectionChanged, this, &OtherFieldsWidget::updateGUI); connect(fieldName, &QLineEdit::textEdited, this, &OtherFieldsWidget::updateGUI); connect(buttonAddApply, &QPushButton::clicked, this, &OtherFieldsWidget::actionAddApply); connect(buttonDelete, &QPushButton::clicked, this, &OtherFieldsWidget::actionDelete); connect(buttonOpen, &QPushButton::clicked, this, &OtherFieldsWidget::actionOpen); } void OtherFieldsWidget::updateList() { const QString selText = otherFieldsList->selectedItems().isEmpty() ? QString() : otherFieldsList->selectedItems().first()->text(0); const QString curText = otherFieldsList->currentItem() == nullptr ? QString() : otherFieldsList->currentItem()->text(0); otherFieldsList->clear(); for (Entry::ConstIterator it = internalEntry->constBegin(); it != internalEntry->constEnd(); ++it) if (!blackListed.contains(it.key().toLower())) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, it.key()); item->setText(1, PlainTextValue::text(it.value())); item->setIcon(0, QIcon::fromTheme(QStringLiteral("entry"))); // FIXME otherFieldsList->addTopLevelItem(item); item->setSelected(selText == it.key()); if (it.key() == curText) otherFieldsList->setCurrentItem(item); } } void OtherFieldsWidget::updateGUI() { QString key = fieldName->text(); if (key.isEmpty() || blackListed.contains(key, Qt::CaseInsensitive)) // TODO check for more (e.g. spaces) buttonAddApply->setEnabled(false); else { buttonAddApply->setEnabled(!isReadOnly); buttonAddApply->setText(internalEntry->contains(key) ? i18n("Apply") : i18n("Add")); buttonAddApply->setIcon(internalEntry->contains(key) ? QIcon::fromTheme(QStringLiteral("document-edit")) : QIcon::fromTheme(QStringLiteral("list-add"))); } } MacroWidget::MacroWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } MacroWidget::~MacroWidget() { delete fieldInputValue; } bool MacroWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer<Macro> macro = element.dynamicCast<Macro>(); if (macro.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); macro->setValue(value); return result; } bool MacroWidget::reset(QSharedPointer<const Element> element) { QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (macro.isNull()) return false; return fieldInputValue->reset(macro->value()); } bool MacroWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void MacroWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldInputValue->setReadOnly(_isReadOnly); } QString MacroWidget::label() { return i18n("Macro"); } QIcon MacroWidget::icon() { return QIcon::fromTheme(QStringLiteral("macro")); } bool MacroWidget::canEdit(const Element *element) { return Macro::isMacro(*element); } void MacroWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast<Qt::Alignment>(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); - fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfSource, this); + fieldInputValue = new FieldInput(KBibTeX::FieldInputType::MultiLine, KBibTeX::TypeFlag::PlainText, KBibTeX::TypeFlag::PlainText | KBibTeX::TypeFlag::Source, this); layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &MacroWidget::gotModified); } PreambleWidget::PreambleWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } bool PreambleWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer<Preamble> preamble = element.dynamicCast<Preamble>(); if (preamble.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); preamble->setValue(value); return result; } bool PreambleWidget::reset(QSharedPointer<const Element> element) { QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (preamble.isNull()) return false; return fieldInputValue->reset(preamble->value()); } bool PreambleWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void PreambleWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); fieldInputValue->setReadOnly(_isReadOnly); } QString PreambleWidget::label() { return i18n("Preamble"); } QIcon PreambleWidget::icon() { return QIcon::fromTheme(QStringLiteral("preamble")); } bool PreambleWidget::canEdit(const Element *element) { return Preamble::isPreamble(*element); } void PreambleWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast<Qt::Alignment>(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); - fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); // FIXME: other editing modes beyond Source applicable? + fieldInputValue = new FieldInput(KBibTeX::FieldInputType::MultiLine, KBibTeX::TypeFlag::Source, KBibTeX::TypeFlag::Source, this); // FIXME: other editing modes beyond Source applicable? layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &PreambleWidget::gotModified); } class SourceWidget::Private { public: QComboBox *messages; QPushButton *buttonRestore; FileImporterBibTeX *importerBibTeX; DelayedExecutionTimer *delayedExecutionTimer; Private(SourceWidget *parent) : messages(nullptr), buttonRestore(nullptr), importerBibTeX(new FileImporterBibTeX(parent)), delayedExecutionTimer(new DelayedExecutionTimer(1500, 500, parent)) { /// nothing } void addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { - const QIcon icon = severity == FileImporter::SeverityInfo ? QIcon::fromTheme(QStringLiteral("dialog-information")) : (severity == FileImporter::SeverityWarning ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : (severity == FileImporter::SeverityError ? QIcon::fromTheme(QStringLiteral("dialog-error")) : QIcon::fromTheme(QStringLiteral("dialog-question")))); + const QIcon icon = severity == FileImporter::MessageSeverity::Info ? QIcon::fromTheme(QStringLiteral("dialog-information")) : (severity == FileImporter::MessageSeverity::Warning ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : (severity == FileImporter::MessageSeverity::Error ? QIcon::fromTheme(QStringLiteral("dialog-error")) : QIcon::fromTheme(QStringLiteral("dialog-question")))); messages->addItem(icon, messageText); } }; SourceWidget::SourceWidget(QWidget *parent) - : ElementWidget(parent), elementClass(elementInvalid), d(new SourceWidget::Private(this)) + : ElementWidget(parent), elementClass(ElementClass::Invalid), d(new SourceWidget::Private(this)) { createGUI(); connect(document, &KTextEditor::Document::textChanged, d->delayedExecutionTimer, &DelayedExecutionTimer::trigger); connect(document, &KTextEditor::Document::textChanged, d->messages, &QComboBox::clear); connect(d->delayedExecutionTimer, &DelayedExecutionTimer::triggered, this, &SourceWidget::updateMessage); } SourceWidget::~SourceWidget() { delete document; delete d; } void SourceWidget::setElementClass(ElementClass elementClass) { this->elementClass = elementClass; updateMessage(); } bool SourceWidget::apply(QSharedPointer<Element> element) const { if (isReadOnly) return false; ///< never save data if in read-only mode const QString text = document->text(); const QScopedPointer<const File> file(d->importerBibTeX->fromString(text)); if (file.isNull() || file->count() != 1) return false; QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); QSharedPointer<Entry> readEntry = file->first().dynamicCast<Entry>(); if (!readEntry.isNull() && !entry.isNull()) { - if (elementClass != elementEntry) return false; ///< Source widget should only edit Entry objects + if (elementClass != ElementClass::Entry) return false; ///< Source widget should only edit Entry objects entry->operator =(*readEntry.data()); //entry = readEntry; return true; } else { QSharedPointer<Macro> macro = element.dynamicCast<Macro>(); QSharedPointer<Macro> readMacro = file->first().dynamicCast<Macro>(); if (!readMacro.isNull() && !macro.isNull()) { - if (elementClass != elementMacro) return false; ///< Source widget should only edit Macro objects + if (elementClass != ElementClass::Macro) return false; ///< Source widget should only edit Macro objects macro->operator =(*readMacro.data()); return true; } else { QSharedPointer<Preamble> preamble = element.dynamicCast<Preamble>(); QSharedPointer<Preamble> readPreamble = file->first().dynamicCast<Preamble>(); if (!readPreamble.isNull() && !preamble.isNull()) { - if (elementClass != elementPreamble) return false; ///< Source widget should only edit Preamble objects + if (elementClass != ElementClass::Preamble) return false; ///< Source widget should only edit Preamble objects preamble->operator =(*readPreamble.data()); return true; } else { qCWarning(LOG_KBIBTEX_GUI) << "Do not know how to apply source code"; return false; } } } } bool SourceWidget::reset(QSharedPointer<const Element> element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value const QSignalBlocker blocker(document); FileExporterBibTeX exporter(this); exporter.setEncoding(QStringLiteral("utf-8")); const QString exportedText = exporter.toString(element, m_file); if (!exportedText.isEmpty()) { originalText = exportedText; /// Limitation of KTextEditor: If editor is read-only, no text can be set /// Therefore, temporarily lift read-only status const bool originalReadWriteStatus = document->isReadWrite(); document->setReadWrite(true); const bool settingTextSuccessful = document->setText(originalText); if (!settingTextSuccessful) qCWarning(LOG_KBIBTEX_GUI) << "Could not set BibTeX source code to source editor"; document->setReadWrite(originalReadWriteStatus); } else qCWarning(LOG_KBIBTEX_GUI) << "Converting entry to BibTeX source resulting in empty text"; return !exportedText.isEmpty(); } bool SourceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); d->messages->clear(); const QString text = document->text(); connect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); const QScopedPointer<const File> file(d->importerBibTeX->fromString(text)); disconnect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); if (file.isNull() || file->count() != 1) { if (widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail message = i18n("Given source code does not parse as one single BibTeX element."); return false; } bool result = false; switch (elementClass) { - case elementEntry: { + case ElementClass::Entry: { QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); result = !entry.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX entry."); } break; - case elementMacro: { + case ElementClass::Macro: { QSharedPointer<Macro> macro = file->first().dynamicCast<Macro>(); result = !macro.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX macro."); } break; - case elementPreamble: { + case ElementClass::Preamble: { QSharedPointer<Preamble> preamble = file->first().dynamicCast<Preamble>(); result = !preamble.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX preamble."); } break; - // case elementComment // TODO? - default: - message = QString(QStringLiteral("elementClass is unknown: %1")).arg(elementClass); + case ElementClass::Comment: { + message = i18n("Comments not supported"); result = false; } + break; + case ElementClass::Invalid: { + message = i18n("Element class not properly set"); + result = false; + } + } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail if (message.isEmpty() && d->messages->count() == 0) - d->addMessage(FileImporter::SeverityInfo, i18n("No issues detected")); + d->addMessage(FileImporter::MessageSeverity::Info, i18n("No issues detected")); return result; } void SourceWidget::setReadOnly(bool _isReadOnly) { ElementWidget::setReadOnly(_isReadOnly); d->buttonRestore->setEnabled(!_isReadOnly); document->setReadWrite(!_isReadOnly); } QString SourceWidget::label() { return i18n("Source"); } QIcon SourceWidget::icon() { return QIcon::fromTheme(QStringLiteral("code-context")); } bool SourceWidget::canEdit(const Element *element) { Q_UNUSED(element) return true; /// source widget should be able to edit any element } void SourceWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); layout->setColumnStretch(0, 1); layout->setColumnStretch(1, 0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); KTextEditor::Editor *editor = KTextEditor::Editor::instance(); document = editor->createDocument(this); document->setHighlightingMode(QStringLiteral("BibTeX")); KTextEditor::View *view = document->createView(this); layout->addWidget(view, 0, 0, 1, 2); d->messages = new QComboBox(this); layout->addWidget(d->messages, 1, 0, 1, 1); d->buttonRestore = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Restore"), this); layout->addWidget(d->buttonRestore, 1, 1, 1, 1); connect(d->buttonRestore, &QPushButton::clicked, this, static_cast<void(SourceWidget::*)()>(&SourceWidget::reset)); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::reset() { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value const QSignalBlocker blocker(document); document->setText(originalText); setModified(false); } void SourceWidget::addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { d->addMessage(severity, messageText); } void SourceWidget::updateMessage() { QString message; const bool validationResult = validate(nullptr, message); if (!message.isEmpty()) { if (validationResult) - addMessage(FileImporter::SeverityInfo, message); + addMessage(FileImporter::MessageSeverity::Info, message); else - addMessage(FileImporter::SeverityError, message); + addMessage(FileImporter::MessageSeverity::Error, message); } } #include "elementwidgets.moc" diff --git a/src/gui/element/elementwidgets.h b/src/gui/element/elementwidgets.h index 75a11963..02ce84db 100644 --- a/src/gui/element/elementwidgets.h +++ b/src/gui/element/elementwidgets.h @@ -1,345 +1,345 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_ELEMENTWIDGETS_H #define KBIBTEX_GUI_ELEMENTWIDGETS_H #include <QLabel> #include <QWidget> #include <QUrl> #include <QIcon> #include <FileImporter> #include <element/ElementEditor> #include <config/EntryLayout> #include "kbibtexgui_export.h" class QTreeWidget; class QTreeWidgetItem; class QBoxLayout; class QGridLayout; class QPushButton; class QLineEdit; class QComboBox; namespace KTextEditor { class Document; } class File; class Entry; class Element; class FieldInput; class ElementWidget : public QWidget { Q_OBJECT public: explicit ElementWidget(QWidget *parent); virtual bool apply(QSharedPointer<Element> element) const = 0; virtual bool reset(QSharedPointer<const Element> element) = 0; virtual bool validate(QWidget **widgetWithIssue, QString &message) const = 0; virtual void setReadOnly(bool isReadOnly) { this->isReadOnly = isReadOnly; } virtual void showReqOptWidgets(bool, const QString &) = 0; virtual QString label() = 0; virtual QIcon icon() = 0; bool isModified() const; void setModified(bool); virtual void setFile(const File *file) { m_file = file; } virtual bool canEdit(const Element *) = 0; protected: bool isReadOnly; const File *m_file; protected slots: void gotModified(); private: bool m_isModified; signals: void modified(bool); }; class EntryConfiguredWidget : public ElementWidget { Q_OBJECT private: typedef struct { QLabel *label; FieldInput *fieldInput; bool isVerticallyMinimumExpaning; } LabeledFieldInput; LabeledFieldInput **listOfLabeledFieldInput; const int fieldInputCount, numCols; QBoxLayout *vboxLayout; QGridLayout *gridLayout; const QSharedPointer<const EntryTabLayout> etl; QMap<QString, FieldInput *> bibtexKeyToWidget; void createGUI(); void layoutGUI(bool forceVisible, const QString &entryType = QString()); public: EntryConfiguredWidget(const QSharedPointer<const EntryTabLayout> &entryTabLayout, QWidget *parent); ~EntryConfiguredWidget() override; bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool forceVisible, const QString &entryType) override; QString identifier() const; QString label() override; QIcon icon() override; void setFile(const File *file) override; bool canEdit(const Element *element) override; signals: void requestingTabChange(const QString &tabIdentifier); private slots: void infoMessageLinkActivated(const QString &contents); }; class ReferenceWidget : public ElementWidget { Q_OBJECT private: QComboBox *entryType; QLineEdit *entryId; QPushButton *buttonSuggestId; void createGUI(); public: explicit ReferenceWidget(QWidget *parent); bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} void setApplyElementInterface(ElementEditor::ApplyElementInterface *applyElement) { m_applyElement = applyElement; } void setOriginalElement(const QSharedPointer<Element> &orig); /** * Return the current value of the entry id/macro key editing widget. * * @return Current value of entry id/macro key if any, otherwise empty string */ QString currentId() const; void setCurrentId(const QString &newId); QString label() override; QIcon icon() override; bool canEdit(const Element *element) override; public slots: void setEntryIdByDefault(); private: ElementEditor::ApplyElementInterface *m_applyElement; bool m_entryIdManuallySet; QSharedPointer<Element> m_element; QString computeType() const; private slots: void prepareSuggestionsMenu(); void insertSuggestionFromAction(); void entryIdManuallyChanged(); signals: void entryTypeChanged(); }; class FilesWidget : public ElementWidget { Q_OBJECT private: FieldInput *fileList; public: explicit FilesWidget(QWidget *parent); bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} QString label() override; QIcon icon() override; void setFile(const File *file) override; bool canEdit(const Element *element) override; private: static const QStringList keyStart; }; class OtherFieldsWidget : public ElementWidget { Q_OBJECT private: QLineEdit *fieldName; FieldInput *fieldContent; QTreeWidget *otherFieldsList; QPushButton *buttonDelete; QPushButton *buttonOpen; QPushButton *buttonAddApply; QUrl currentUrl; const QStringList blackListed; QSharedPointer<Entry> internalEntry; QStringList deletedKeys, modifiedKeys; bool m_isReadOnly; void createGUI(); void updateList(); public: OtherFieldsWidget(const QStringList &blacklistedFields, QWidget *parent); ~OtherFieldsWidget() override; bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} QString label() override; QIcon icon() override; bool canEdit(const Element *element) override; private slots: void listElementExecuted(QTreeWidgetItem *item, int column); void listCurrentChanged(QTreeWidgetItem *item, QTreeWidgetItem *previous); void actionAddApply(); void actionDelete(); void actionOpen(); void updateGUI(); }; class MacroWidget : public ElementWidget { Q_OBJECT private: FieldInput *fieldInputValue; void createGUI(); public: explicit MacroWidget(QWidget *parent); ~MacroWidget() override; bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} QString label() override; QIcon icon() override; bool canEdit(const Element *element) override; }; class PreambleWidget : public ElementWidget { Q_OBJECT private: FieldInput *fieldInputValue; void createGUI(); public: explicit PreambleWidget(QWidget *parent); bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} QString label() override; QIcon icon() override; bool canEdit(const Element *element) override; }; class SourceWidget : public ElementWidget { Q_OBJECT public: - enum ElementClass { elementInvalid = -1, elementEntry = 0, elementMacro, elementPreamble, elementComment }; + enum class ElementClass { Invalid = -1, Entry = 0, Macro, Preamble, Comment }; private: KTextEditor::Document *document; QString originalText; ElementClass elementClass; void createGUI(); public: explicit SourceWidget(QWidget *parent); ~SourceWidget() override; void setElementClass(ElementClass elementClass); bool apply(QSharedPointer<Element> element) const override; bool reset(QSharedPointer<const Element> element) override; bool validate(QWidget **widgetWithIssue, QString &message) const override; void setReadOnly(bool isReadOnly) override; void showReqOptWidgets(bool, const QString &) override {} QString label() override; QIcon icon() override; bool canEdit(const Element *element) override; private slots: void reset(); void addMessage(const FileImporter::MessageSeverity severity, const QString &messageText); void updateMessage(); private: class Private; Private *const d; }; #endif // KBIBTEX_GUI_ELEMENTWIDGETS_H diff --git a/src/gui/element/findpdfui.cpp b/src/gui/element/findpdfui.cpp index f4584e20..65ca2ccc 100644 --- a/src/gui/element/findpdfui.cpp +++ b/src/gui/element/findpdfui.cpp @@ -1,530 +1,530 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "findpdfui.h" #include "findpdfui_p.h" #include <QGridLayout> #include <QListView> #include <QAbstractListModel> #include <QUrl> #include <QPainter> #include <QToolButton> #include <QApplication> #include <QAction> #include <QButtonGroup> #include <QRadioButton> #include <QLabel> #include <QPointer> #include <QPushButton> #include <QFileDialog> #include <QDialog> #include <QDialogButtonBox> #include <QMimeType> #include <QTemporaryFile> #include <KLocalizedString> #include <KIconLoader> #include <KSqueezedTextLabel> #include <KRun> #include <kio_version.h> #include <Preferences> #include <FileInfo> #include "field/fieldlistedit.h" #include "logging_gui.h" class PDFListModel; const int posLabelUrl = 0; const int posLabelPreview = 1; const int posViewButton = 2; const int posRadioNoDownload = 3; const int posRadioDownload = 4; const int posRadioURLonly = 5; /// inspired by KNewStuff3's ItemsViewDelegate PDFItemDelegate::PDFItemDelegate(QListView *itemView, QObject *parent) : KWidgetItemDelegate(itemView, parent), m_parent(itemView) { /// nothing } void PDFItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyle *style = QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); painter->save(); if (option.state & QStyle::State_Selected) { painter->setPen(QPen(option.palette.highlightedText().color())); } else { painter->setPen(QPen(option.palette.text().color())); } /// draw icon based on mime-type QPixmap icon = index.data(Qt::DecorationRole).value<QPixmap>(); if (!icon.isNull()) { int margin = option.fontMetrics.height() / 3; painter->drawPixmap(margin, margin + option.rect.top(), KIconLoader::SizeMedium, KIconLoader::SizeMedium, icon); } painter->restore(); } QList<QWidget *> PDFItemDelegate::createItemWidgets(const QModelIndex &index) const { Q_UNUSED(index) // FIXME really of no use? QList<QWidget *> list; /// first, the label with shows the found PDF file's origin (URL) KSqueezedTextLabel *label = new KSqueezedTextLabel(); label->setBackgroundRole(QPalette::NoRole); label->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << label; Q_ASSERT_X(list.count() == posLabelUrl + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelUrl + 1"); /// a label with shows either the PDF's title or a text snipplet QLabel *previewLabel = new QLabel(); previewLabel->setBackgroundRole(QPalette::NoRole); previewLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << previewLabel; Q_ASSERT_X(list.count() == posLabelPreview + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelPreview + 1"); /// add a push button to view the PDF file QPushButton *pushButton = new QPushButton(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View")); list << pushButton; connect(pushButton, &QPushButton::clicked, this, &PDFItemDelegate::slotViewPDF); Q_ASSERT_X(list.count() == posViewButton + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posViewButton + 1"); /// a button group to choose what to do with this particular PDF file QButtonGroup *bg = new QButtonGroup(); /// button group's first choice: ignore file (discard it) QRadioButton *radioButton = new QRadioButton(i18n("Ignore")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioNoDownloadToggled); Q_ASSERT_X(list.count() == posRadioNoDownload + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioNoDownload + 1"); /// download this file and store it locally, user will be asked for "Save As" radioButton = new QRadioButton(i18n("Download")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioDownloadToggled); Q_ASSERT_X(list.count() == posRadioDownload + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioDownload + 1"); /// paste URL into BibTeX entry, no local copy is stored radioButton = new QRadioButton(i18n("Use URL only")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioURLonlyToggled); Q_ASSERT_X(list.count() == posRadioURLonly + 1, "QList<QWidget *> PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioURLonly + 1"); return list; } /// Update the widgets /// Clazy warns: "Missing reference on non-trivial type" for argument 'widgets', /// but KWidgetItemDelegate defines this function this way and cannot be changed. void PDFItemDelegate::updateItemWidgets(const QList<QWidget *> widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const { if (!index.isValid()) return; const PDFListModel *model = qobject_cast<const PDFListModel *>(index.model()); if (model == nullptr) { qCDebug(LOG_KBIBTEX_GUI) << "WARNING - INVALID MODEL!"; return; } /// determine some variables used for layout const int margin = option.fontMetrics.height() / 3; const int buttonHeight = option.fontMetrics.height() * 6 / 3; #if QT_VERSION >= 0x050b00 const int maxTextWidth = qMax(qMax(option.fontMetrics.horizontalAdvance(i18n("Use URL only")), option.fontMetrics.horizontalAdvance(i18n("Ignore"))), qMax(option.fontMetrics.horizontalAdvance(i18n("Download")), option.fontMetrics.horizontalAdvance(i18n("View")))); #else // QT_VERSION >= 0x050b00 const int maxTextWidth = qMax(qMax(option.fontMetrics.width(i18n("Use URL only")), option.fontMetrics.width(i18n("Ignore"))), qMax(option.fontMetrics.width(i18n("Download")), option.fontMetrics.width(i18n("View")))); #endif // QT_VERSION >= 0x050b00 const int buttonWidth = maxTextWidth * 3 / 2; const int labelWidth = option.rect.width() - 3 * margin - KIconLoader::SizeMedium; const int labelHeight = option.fontMetrics.height();//(option.rect.height() - 4 * margin - buttonHeight) / 2; /// Total height = margin + labelHeight + margin + labelHeight + marin + buttonHeight + margin /// = option.fontMetrics.height() * (1/3 + 1 + 1/3 + 1 + 1/3 + 6/3 + 1/3) /// = option.fontMetrics.height() * 16 / 3 /// setup label which will show the PDF file's URL KSqueezedTextLabel *label = qobject_cast<KSqueezedTextLabel *>(widgets[posLabelUrl]); if (label != nullptr) { const QString text = index.data(PDFListModel::URLRole).toUrl().toDisplayString(); label->setText(text); label->setToolTip(text); label->move(margin * 2 + KIconLoader::SizeMedium, margin); label->resize(labelWidth, labelHeight); } /// setup label which will show the PDF's title or textual beginning QLabel *previewLabel = qobject_cast<QLabel *>(widgets[posLabelPreview]); if (previewLabel != nullptr) { previewLabel->setText(index.data(PDFListModel::TextualPreviewRole).toString()); previewLabel->move(margin * 2 + KIconLoader::SizeMedium, margin * 2 + labelHeight); previewLabel->resize(labelWidth, labelHeight); } /// setup the view button QPushButton *viewButton = qobject_cast<QPushButton *>(widgets[posViewButton]); if (viewButton != nullptr) { const QSize hint = viewButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; viewButton->move(margin * 2 + KIconLoader::SizeMedium, option.rect.height() - margin - h); viewButton->resize(buttonWidth, h); } /// setup each of the three radio buttons for (int i = 0; i < 3; ++i) { QRadioButton *radioButton = qobject_cast<QRadioButton *>(widgets[posRadioNoDownload + i]); if (radioButton != nullptr) { const QSize hint = radioButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; radioButton->move(option.rect.width() - margin - (3 - i) * (buttonWidth + margin), option.rect.height() - margin - h); radioButton->resize(buttonWidth, h); bool ok = false; - radioButton->setChecked(i + FindPDF::NoDownload == index.data(PDFListModel::DownloadModeRole).toInt(&ok) && ok); + radioButton->setChecked(i + static_cast<int>(FindPDF::DownloadMode::No) == index.data(PDFListModel::DownloadModeRole).toInt(&ok) && ok); } } } QSize PDFItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const { /// set a size that is suiteable QSize size; #if QT_VERSION >= 0x050b00 size.setWidth(option.fontMetrics.horizontalAdvance(i18n("Download")) * 6); #else // QT_VERSION >= 0x050b00 size.setWidth(option.fontMetrics.width(i18n("Download")) * 6); #endif // QT_VERSION >= 0x050b00 size.setHeight(qMax(option.fontMetrics.height() * 16 / 3, static_cast<int>(KIconLoader::SizeMedium))); ///< KIconLoader::SizeMedium should be 32 return size; } /** * Method is called when the "View PDF" button of a list item is clicked. * Opens the associated URL or its local copy using the system's default viewer. */ void PDFItemDelegate::slotViewPDF() { QModelIndex index = focusedIndex(); if (index.isValid()) { const QString tempfileName = index.data(PDFListModel::TempFileNameRole).toString(); const QUrl url = index.data(PDFListModel::URLRole).toUrl(); if (!tempfileName.isEmpty()) { /// Guess mime type for url to open QUrl tempUrl(tempfileName); QMimeType mimeType = FileInfo::mimeTypeForUrl(tempUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(tempUrl, mimeTypeName, itemView(), KRun::RunFlags(), url.toDisplayString()); } else if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, itemView(), KRun::RunFlags()); } } } /** * Updated the model when the user selects the radio button for ignoring a PDF file. */ void PDFItemDelegate::slotRadioNoDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { - m_parent->model()->setData(index, FindPDF::NoDownload, PDFListModel::DownloadModeRole); + m_parent->model()->setData(index, static_cast<int>(FindPDF::DownloadMode::No), PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for downloading a PDF file. */ void PDFItemDelegate::slotRadioDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { - m_parent->model()->setData(index, FindPDF::Download, PDFListModel::DownloadModeRole); + m_parent->model()->setData(index, static_cast<int>(FindPDF::DownloadMode::PDFfile), PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for keeping a PDF file's URL. */ void PDFItemDelegate::slotRadioURLonlyToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { - m_parent->model()->setData(index, FindPDF::URLonly, PDFListModel::DownloadModeRole); + m_parent->model()->setData(index, static_cast<int>(FindPDF::DownloadMode::URLonly), PDFListModel::DownloadModeRole); } } PDFListModel::PDFListModel(QList<FindPDF::ResultItem> &resultList, QObject *parent) : QAbstractListModel(parent), m_resultList(resultList) { /// nothing } int PDFListModel::rowCount(const QModelIndex &parent) const { /// row cout depends on number of found PDF references int count = parent == QModelIndex() ? m_resultList.count() : 0; return count; } QVariant PDFListModel::data(const QModelIndex &index, int role) const { if (index != QModelIndex() && index.parent() == QModelIndex() && index.row() < m_resultList.count()) { if (role == Qt::DisplayRole) return m_resultList[index.row()].url.toDisplayString(); else if (role == URLRole) return m_resultList[index.row()].url; else if (role == TextualPreviewRole) return m_resultList[index.row()].textPreview; else if (role == Qt::ToolTipRole) return QStringLiteral("<qt>") + m_resultList[index.row()].textPreview + QStringLiteral("</qt>"); ///< 'qt' tags required for word wrap else if (role == TempFileNameRole) { if (m_resultList[index.row()].tempFilename != nullptr) return m_resultList[index.row()].tempFilename->fileName(); else return QVariant(); } else if (role == DownloadModeRole) - return m_resultList[index.row()].downloadMode; + return static_cast<int>(m_resultList[index.row()].downloadMode); else if (role == Qt::DecorationRole) { /// make an educated guess on the icon, based on URL or path QString iconName = FileInfo::mimeTypeForUrl(m_resultList[index.row()].url).iconName(); iconName = iconName == QStringLiteral("application-octet-stream") ? QStringLiteral("application-pdf") : iconName; return QIcon::fromTheme(iconName).pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium); } else return QVariant(); } return QVariant(); } bool PDFListModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index != QModelIndex() && index.row() < m_resultList.count() && role == DownloadModeRole) { bool ok = false; const FindPDF::DownloadMode downloadMode = static_cast<FindPDF::DownloadMode>(value.toInt(&ok)); if (ok) { m_resultList[index.row()].downloadMode = downloadMode; return true; } } return false; } QVariant PDFListModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation) if (section == 0) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { return i18n("Result"); } else return QVariant(); } return QVariant(); } class FindPDFUI::Private { private: FindPDFUI *p; public: QListView *listViewResult; QLabel *labelMessage; QList<FindPDF::ResultItem> resultList; FindPDF *findpdf; Private(FindPDFUI *parent) : p(parent), findpdf(new FindPDF(parent)) { setupGUI(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); const int minWidth = p->fontMetrics().height() * 40; const int minHeight = p->fontMetrics().height() * 20; p->setMinimumSize(minWidth, minHeight); listViewResult = new QListView(p); layout->addWidget(listViewResult, 0, 0); listViewResult->setEnabled(false); listViewResult->hide(); labelMessage = new QLabel(p); layout->addWidget(labelMessage, 1, 0); labelMessage->setMinimumSize(minWidth, minHeight); labelMessage->setAlignment(Qt::AlignVCenter | Qt::AlignCenter); static_cast<QWidget *>(p->parent())->setCursor(Qt::WaitCursor); } }; FindPDFUI::FindPDFUI(Entry &entry, QWidget *parent) : QWidget(parent), d(new Private(this)) { d->labelMessage->show(); d->labelMessage->setText(i18n("Starting to search...")); connect(d->findpdf, &FindPDF::finished, this, &FindPDFUI::searchFinished); connect(d->findpdf, &FindPDF::progress, this, &FindPDFUI::searchProgress); d->findpdf->search(entry); } FindPDFUI::~FindPDFUI() { for (QList<FindPDF::ResultItem>::Iterator it = d->resultList.begin(); it != d->resultList.end();) { delete it->tempFilename; it = d->resultList.erase(it); } } void FindPDFUI::interactiveFindPDF(Entry &entry, const File &bibtexFile, QWidget *parent) { QPointer<QDialog> dlg = new QDialog(parent); QPointer<FindPDFUI> widget = new FindPDFUI(entry, dlg); dlg->setWindowTitle(i18n("Find PDF")); QBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(widget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Abort | QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); dlg->setLayout(layout); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Ok), &QWidget::setEnabled); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Abort), &QWidget::setDisabled); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); connect(buttonBox->button(QDialogButtonBox::Abort), &QPushButton::clicked, widget.data(), &FindPDFUI::stopSearch); if (dlg->exec() == QDialog::Accepted) widget->apply(entry, bibtexFile); delete dlg; } void FindPDFUI::apply(Entry &entry, const File &bibtexFile) { QAbstractItemModel *model = d->listViewResult->model(); for (int i = 0; i < model->rowCount(); ++i) { bool ok = false; FindPDF::DownloadMode downloadMode = static_cast<FindPDF::DownloadMode>(model->data(model->index(i, 0), PDFListModel::DownloadModeRole).toInt(&ok)); if (!ok) { qCDebug(LOG_KBIBTEX_GUI) << "Could not interprete download mode"; - downloadMode = FindPDF::NoDownload; + downloadMode = FindPDF::DownloadMode::No; } QUrl url = model->data(model->index(i, 0), PDFListModel::URLRole).toUrl(); QString tempfileName = model->data(model->index(i, 0), PDFListModel::TempFileNameRole).toString(); - if (downloadMode == FindPDF::URLonly && url.isValid()) { + if (downloadMode == FindPDF::DownloadMode::URLonly && url.isValid()) { bool alreadyContained = false; for (QMap<QString, Value>::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) // FIXME this will terribly break if URLs in an entry's URL field are separated with semicolons alreadyContained |= it.key().toLower().startsWith(Entry::ftUrl) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer<VerbatimText>(new VerbatimText(url.toDisplayString()))); if (!entry.contains(Entry::ftUrl)) entry.insert(Entry::ftUrl, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(Entry::ftUrl).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } - } else if (downloadMode == FindPDF::Download && !tempfileName.isEmpty()) { + } else if (downloadMode == FindPDF::DownloadMode::PDFfile && !tempfileName.isEmpty()) { QUrl startUrl = bibtexFile.property(File::Url, QUrl()).toUrl(); const QString absoluteFilename = QFileDialog::getSaveFileName(this, i18n("Save URL '%1'", url.url(QUrl::PreferLocalFile)), startUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(), QStringLiteral("application/pdf")); if (!absoluteFilename.isEmpty()) { const QString visibleFilename = UrlListEdit::askRelativeOrStaticFilename(this, absoluteFilename, startUrl); qCDebug(LOG_KBIBTEX_GUI) << "Saving PDF from " << url << " to file " << absoluteFilename << " known as " << visibleFilename; // FIXME test for overwrite QFile::copy(tempfileName, absoluteFilename); bool alreadyContained = false; for (QMap<QString, Value>::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) alreadyContained |= (it.key().toLower().startsWith(Entry::ftFile) || it.key().toLower().startsWith(Entry::ftLocalFile) || it.key().toLower().startsWith(Entry::ftUrl)) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer<VerbatimText>(new VerbatimText(visibleFilename))); - const QString fieldNameStem = Preferences::instance().bibliographySystem() == Preferences::BibTeX ? Entry::ftLocalFile : Entry::ftFile; + const QString fieldNameStem = Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibTeX ? Entry::ftLocalFile : Entry::ftFile; if (!entry.contains(fieldNameStem)) entry.insert(fieldNameStem, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(fieldNameStem).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } } } } } void FindPDFUI::searchFinished() { d->labelMessage->hide(); d->listViewResult->show(); d->resultList = d->findpdf->results(); d->listViewResult->setModel(new PDFListModel(d->resultList, d->listViewResult)); d->listViewResult->setItemDelegate(new PDFItemDelegate(d->listViewResult, d->listViewResult)); d->listViewResult->setEnabled(true); d->listViewResult->reset(); static_cast<QWidget *>(parent())->unsetCursor(); emit resultAvailable(true); } void FindPDFUI::searchProgress(int visitedPages, int runningJobs, int foundDocuments) { d->listViewResult->hide(); d->labelMessage->show(); d->labelMessage->setText(i18n("<qt>Number of visited pages: <b>%1</b><br/>Number of running downloads: <b>%2</b><br/>Number of found documents: <b>%3</b></qt>", visitedPages, runningJobs, foundDocuments)); } void FindPDFUI::stopSearch() { d->findpdf->abort(); searchFinished(); } diff --git a/src/gui/field/fieldinput.cpp b/src/gui/field/fieldinput.cpp index 73d9bb8a..e31381fd 100644 --- a/src/gui/field/fieldinput.cpp +++ b/src/gui/field/fieldinput.cpp @@ -1,382 +1,382 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fieldinput.h" #include <QLayout> #include <QApplication> #include <QMenu> #include <QDate> #include <QSpinBox> #include <QPushButton> #include <QInputDialog> #include <QSignalBlocker> #include <KLocalizedString> #include <File> #include <Entry> #include "fieldlineedit.h" #include "fieldlistedit.h" #include "colorlabelwidget.h" #include "widgets/starrating.h" class FieldInput::FieldInputPrivate { private: FieldInput *p; public: ColorLabelWidget *colorWidget; StarRatingFieldInput *starRatingWidget; FieldLineEdit *fieldLineEdit; FieldListEdit *fieldListEdit; KBibTeX::FieldInputType fieldInputType; KBibTeX::TypeFlags typeFlags; KBibTeX::TypeFlag preferredTypeFlag; const File *bibtexFile; const Element *element; FieldInputPrivate(FieldInput *parent) - : p(parent), colorWidget(nullptr), starRatingWidget(nullptr), fieldLineEdit(nullptr), fieldListEdit(nullptr), fieldInputType(KBibTeX::SingleLine), preferredTypeFlag(KBibTeX::tfSource), bibtexFile(nullptr), element(nullptr) { + : p(parent), colorWidget(nullptr), starRatingWidget(nullptr), fieldLineEdit(nullptr), fieldListEdit(nullptr), fieldInputType(KBibTeX::FieldInputType::SingleLine), preferredTypeFlag(KBibTeX::TypeFlag::Source), bibtexFile(nullptr), element(nullptr) { /// nothing } ~FieldInputPrivate() { if (colorWidget != nullptr) delete colorWidget; else if (starRatingWidget != nullptr) delete starRatingWidget; else if (fieldLineEdit != nullptr) delete fieldLineEdit; else if (fieldListEdit != nullptr) delete fieldListEdit; } void createGUI() { QHBoxLayout *layout = new QHBoxLayout(p); layout->setMargin(0); switch (fieldInputType) { - case KBibTeX::MultiLine: + case KBibTeX::FieldInputType::MultiLine: fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, true, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); break; - case KBibTeX::List: + case KBibTeX::FieldInputType::List: fieldListEdit = new FieldListEdit(preferredTypeFlag, typeFlags, p); connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; - case KBibTeX::Month: { + case KBibTeX::FieldInputType::Month: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *monthSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("view-calendar-month")), QString()); monthSelector->setToolTip(i18n("Select a predefined month")); fieldLineEdit->prependWidget(monthSelector); QMenu *monthMenu = new QMenu(monthSelector); for (int i = 1; i <= 12; ++i) { QAction *monthAction = monthMenu->addAction(QLocale::system().standaloneMonthName(i)); connect(monthAction, &QAction::triggered, p, [this, i]() { setMonth(i); }); } monthSelector->setMenu(monthMenu); } break; - case KBibTeX::Edition: { + case KBibTeX::FieldInputType::Edition: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *editionSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("clock")), QString()); editionSelector->setToolTip(i18n("Select a predefined edition")); fieldLineEdit->prependWidget(editionSelector); QMenu *editionMenu = new QMenu(editionSelector); static const QStringList ordinals{i18n("1st"), i18n("2nd"), i18n("3rd"), i18n("4th"), i18n("5th"), i18n("6th"), i18n("7th"), i18n("8th"), i18n("9th"), i18n("10th"), i18n("11th"), i18n("12th"), i18n("13th"), i18n("14th"), i18n("15th"), i18n("16th")}; for (int i = 0; i < ordinals.length(); ++i) { QAction *editionAction = editionMenu->addAction(ordinals[i]); connect(editionAction, &QAction::triggered, p, [this, i]() { setEdition(i + 1); }); } editionSelector->setMenu(editionMenu); } break; - case KBibTeX::CrossRef: { + case KBibTeX::FieldInputType::CrossRef: { fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); QPushButton *referenceSelector = new QPushButton(QIcon::fromTheme(QStringLiteral("flag-green")), QString()); ///< find better icon referenceSelector->setToolTip(i18n("Select an existing entry")); fieldLineEdit->prependWidget(referenceSelector); connect(referenceSelector, &QPushButton::clicked, p, &FieldInput::selectCrossRef); } break; - case KBibTeX::Color: { + case KBibTeX::FieldInputType::Color: { colorWidget = new ColorLabelWidget(p); connect(colorWidget, &ColorLabelWidget::modified, p, &FieldInput::modified); layout->addWidget(colorWidget, 0); } break; - case KBibTeX::StarRating: { + case KBibTeX::FieldInputType::StarRating: { starRatingWidget = new StarRatingFieldInput(8 /* = #stars */, p); connect(starRatingWidget, &StarRatingFieldInput::modified, p, &FieldInput::modified); layout->addWidget(starRatingWidget, 0); } break; - case KBibTeX::PersonList: + case KBibTeX::FieldInputType::PersonList: fieldListEdit = new PersonListEdit(preferredTypeFlag, typeFlags, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; - case KBibTeX::UrlList: + case KBibTeX::FieldInputType::UrlList: fieldListEdit = new UrlListEdit(p); connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; - case KBibTeX::KeywordList: + case KBibTeX::FieldInputType::KeywordList: fieldListEdit = new KeywordListEdit(p); connect(fieldListEdit, &FieldListEdit::modified, p, &FieldInput::modified); layout->addWidget(fieldListEdit); break; default: fieldLineEdit = new FieldLineEdit(preferredTypeFlag, typeFlags, false, p); connect(fieldLineEdit, &FieldLineEdit::textChanged, p, &FieldInput::modified); layout->addWidget(fieldLineEdit); } } void clear() { if (fieldLineEdit != nullptr) { const QSignalBlocker blocker(fieldLineEdit); fieldLineEdit->setText(QString()); } else if (fieldListEdit != nullptr) { const QSignalBlocker blocker(fieldListEdit); fieldListEdit->clear(); } else if (colorWidget != nullptr) { const QSignalBlocker blocker(colorWidget); colorWidget->clear(); } else if (starRatingWidget != nullptr) { const QSignalBlocker blocker(starRatingWidget); starRatingWidget->unsetValue(); } } bool reset(const Value &value) { bool result = false; if (fieldLineEdit != nullptr) { const QSignalBlocker blocker(fieldLineEdit); result = fieldLineEdit->reset(value); } else if (fieldListEdit != nullptr) { const QSignalBlocker blocker(fieldListEdit); result = fieldListEdit->reset(value); } else if (colorWidget != nullptr) { const QSignalBlocker blocker(colorWidget); result = colorWidget->reset(value); } else if (starRatingWidget != nullptr) { const QSignalBlocker blocker(starRatingWidget); result = starRatingWidget->reset(value); } return result; } bool apply(Value &value) const { bool result = false; if (fieldLineEdit != nullptr) result = fieldLineEdit->apply(value); else if (fieldListEdit != nullptr) result = fieldListEdit->apply(value); else if (colorWidget != nullptr) result = colorWidget->apply(value); else if (starRatingWidget != nullptr) result = starRatingWidget->apply(value); return result; } bool validate(QWidget **widgetWithIssue, QString &message) const { if (fieldLineEdit != nullptr) return fieldLineEdit->validate(widgetWithIssue, message); else if (fieldListEdit != nullptr) return fieldListEdit->validate(widgetWithIssue, message); else if (colorWidget != nullptr) return colorWidget->validate(widgetWithIssue, message); else if (starRatingWidget != nullptr) return starRatingWidget->validate(widgetWithIssue, message); return false; } void setReadOnly(bool isReadOnly) { if (fieldLineEdit != nullptr) fieldLineEdit->setReadOnly(isReadOnly); else if (fieldListEdit != nullptr) fieldListEdit->setReadOnly(isReadOnly); else if (colorWidget != nullptr) colorWidget->setReadOnly(isReadOnly); else if (starRatingWidget != nullptr) starRatingWidget->setReadOnly(isReadOnly); } void setFile(const File *file) { bibtexFile = file; if (fieldLineEdit != nullptr) fieldLineEdit->setFile(file); if (fieldListEdit != nullptr) fieldListEdit->setFile(file); } void setElement(const Element *element) { this->element = element; if (fieldLineEdit != nullptr) fieldLineEdit->setElement(element); if (fieldListEdit != nullptr) fieldListEdit->setElement(element); } void setFieldKey(const QString &fieldKey) { if (fieldLineEdit != nullptr) fieldLineEdit->setFieldKey(fieldKey); if (fieldListEdit != nullptr) fieldListEdit->setFieldKey(fieldKey); } void setCompletionItems(const QStringList &items) { if (fieldLineEdit != nullptr) fieldLineEdit->setCompletionItems(items); if (fieldListEdit != nullptr) fieldListEdit->setCompletionItems(items); } bool selectCrossRef() { Q_ASSERT_X(fieldLineEdit != nullptr, "void FieldInput::FieldInputPrivate::selectCrossRef()", "fieldLineEdit is invalid"); if (bibtexFile == nullptr) return false; /// create a standard input dialog with a list of all keys (ids of entries) bool ok = false; - QStringList list = bibtexFile->allKeys(File::etEntry); + QStringList list = bibtexFile->allKeys(File::ElementType::Entry); list.sort(); /// remove own id const Entry *entry = dynamic_cast<const Entry *>(element); if (entry != nullptr) list.removeOne(entry->id()); QString crossRef = QInputDialog::getItem(p, i18n("Select Cross Reference"), i18n("Select the cross reference to another entry:"), list, 0, false, &ok); if (ok && !crossRef.isEmpty()) { /// insert selected cross reference into edit widget Value value; value.append(QSharedPointer<VerbatimText>(new VerbatimText(crossRef))); reset(value); return true; } return false; } void setMonth(int month) { Value value; value.append(QSharedPointer<MacroKey>(new MacroKey(KBibTeX::MonthsTriple[month - 1]))); reset(value); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } void setEdition(int edition) { Value value; value.append(QSharedPointer<MacroKey>(new MacroKey(QString::number(edition)))); reset(value); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }; FieldInput::FieldInput(KBibTeX::FieldInputType fieldInputType, KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : QWidget(parent), d(new FieldInputPrivate(this)) { d->fieldInputType = fieldInputType; d->typeFlags = typeFlags; d->preferredTypeFlag = preferredTypeFlag; d->createGUI(); } FieldInput::~FieldInput() { delete d; } void FieldInput::clear() { d->clear(); } bool FieldInput::reset(const Value &value) { return d->reset(value); } bool FieldInput::apply(Value &value) const { return d->apply(value); } bool FieldInput::validate(QWidget **widgetWithIssue, QString &message) const { return d->validate(widgetWithIssue, message); } void FieldInput::setReadOnly(bool isReadOnly) { d->setReadOnly(isReadOnly); } void FieldInput::setFile(const File *file) { d->setFile(file); } void FieldInput::setElement(const Element *element) { d->setElement(element); } void FieldInput::setFieldKey(const QString &fieldKey) { d->setFieldKey(fieldKey); } void FieldInput::setCompletionItems(const QStringList &items) { d->setCompletionItems(items); } QWidget *FieldInput::buddy() { if (d->fieldLineEdit != nullptr) return d->fieldLineEdit->buddy(); // TODO fieldListEdit else if (d->colorWidget != nullptr) return d->colorWidget; else if (d->starRatingWidget != nullptr) return d->starRatingWidget; return nullptr; } void FieldInput::selectCrossRef() { if (d->selectCrossRef()) emit modified(); } diff --git a/src/gui/field/fieldlineedit.cpp b/src/gui/field/fieldlineedit.cpp index 710fd912..f09b7173 100644 --- a/src/gui/field/fieldlineedit.cpp +++ b/src/gui/field/fieldlineedit.cpp @@ -1,550 +1,550 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fieldlineedit.h" #include <typeinfo> #include <QMenu> #include <QBuffer> #include <QFileInfo> #include <QDir> #include <QDragEnterEvent> #include <QDropEvent> #include <QPushButton> #include <QFontDatabase> #include <QUrl> #include <QMimeType> #include <QMimeData> #include <QRegularExpression> #include <KRun> #include <KMessageBox> #include <KLocalizedString> #include <kio_version.h> #include <BibTeXFields> #include <Preferences> #include <File> #include <Entry> #include <Value> #include <FileInfo> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <EncoderLaTeX> #include "logging_gui.h" class FieldLineEdit::FieldLineEditPrivate { private: FieldLineEdit *parent; Value currentValue; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; QPushButton *buttonOpenUrl; public: QMenu *menuTypes; QUrl urlToOpen; const File *file; QString fieldKey; KBibTeX::TypeFlag typeFlag; FieldLineEditPrivate(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldLineEdit *p) : parent(p), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr) { menuTypes = new QMenu(parent); setupMenu(); buttonOpenUrl = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open-remote")), QString(), parent); buttonOpenUrl->setVisible(false); buttonOpenUrl->setProperty("isConst", true); parent->appendWidget(buttonOpenUrl); connect(buttonOpenUrl, &QPushButton::clicked, parent, [this]() { openUrl(); }); connect(p, &FieldLineEdit::textChanged, p, [this](const QString & text) { textChanged(text); }); Value value; typeFlag = determineTypeFlag(value, preferredTypeFlag, typeFlags); updateGUI(typeFlag); } bool reset(const Value &value) { bool result = false; QString text; typeFlag = determineTypeFlag(value, typeFlag, typeFlags); updateGUI(typeFlag); if (!value.isEmpty()) { - if (typeFlag == KBibTeX::tfSource) { + if (typeFlag == KBibTeX::TypeFlag::Source) { /// simple case: field's value is to be shown as BibTeX code, including surrounding curly braces FileExporterBibTeX exporter(parent); text = exporter.valueToBibTeX(value); result = true; } else { /// except for the source view type flag, type flag views do not support composed values, /// therefore only the first value will be shown const QSharedPointer<ValueItem> first = value.first(); const QSharedPointer<PlainText> plainText = first.dynamicCast<PlainText>(); - if (typeFlag == KBibTeX::tfPlainText && !plainText.isNull()) { + if (typeFlag == KBibTeX::TypeFlag::PlainText && !plainText.isNull()) { text = plainText->text(); result = true; } else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); - if (typeFlag == KBibTeX::tfPerson && !person.isNull()) { + if (typeFlag == KBibTeX::TypeFlag::Person && !person.isNull()) { text = Person::transcribePersonName(person.data(), Preferences::instance().personNameFormat()); result = true; } else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); - if (typeFlag == KBibTeX::tfReference && !macroKey.isNull()) { + if (typeFlag == KBibTeX::TypeFlag::Reference && !macroKey.isNull()) { text = macroKey->text(); result = true; } else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); - if (typeFlag == KBibTeX::tfKeyword && !keyword.isNull()) { + if (typeFlag == KBibTeX::TypeFlag::Keyword && !keyword.isNull()) { text = keyword->text(); result = true; } else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); - if (typeFlag == KBibTeX::tfVerbatim && !verbatimText.isNull()) { + if (typeFlag == KBibTeX::TypeFlag::Verbatim && !verbatimText.isNull()) { text = verbatimText->text(); result = true; } else - qCWarning(LOG_KBIBTEX_GUI) << "Could not reset: " << typeFlag << "(" << (typeFlag == KBibTeX::tfSource ? "Source" : (typeFlag == KBibTeX::tfReference ? "Reference" : (typeFlag == KBibTeX::tfPerson ? "Person" : (typeFlag == KBibTeX::tfPlainText ? "PlainText" : (typeFlag == KBibTeX::tfKeyword ? "Keyword" : (typeFlag == KBibTeX::tfVerbatim ? "Verbatim" : "???")))))) << ") " << (typeFlags.testFlag(KBibTeX::tfPerson) ? "Person" : "") << (typeFlags.testFlag(KBibTeX::tfPlainText) ? "PlainText" : "") << (typeFlags.testFlag(KBibTeX::tfReference) ? "Reference" : "") << (typeFlags.testFlag(KBibTeX::tfVerbatim) ? "Verbatim" : "") << " " << typeid((void)*first).name() << " : " << PlainTextValue::text(value); + qCWarning(LOG_KBIBTEX_GUI) << "Could not reset: " << typeFlag << " " << typeid((void)*first).name() << " : " << PlainTextValue::text(value); } } } } } } updateURL(text); parent->setText(text); return result; } bool apply(Value &value) const { value.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified - const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); + const QString text = typeFlag == KBibTeX::TypeFlag::Source || typeFlag == KBibTeX::TypeFlag::Verbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); static const QRegularExpression invalidCharsForReferenceRegExp(QStringLiteral("[^-_:/a-zA-Z0-9]")); if (encodedText.isEmpty()) return true; - else if (typeFlag == KBibTeX::tfPlainText) { + else if (typeFlag == KBibTeX::TypeFlag::PlainText) { value.append(QSharedPointer<PlainText>(new PlainText(encodedText))); return true; - } else if (typeFlag == KBibTeX::tfReference && !encodedText.contains(invalidCharsForReferenceRegExp)) { + } else if (typeFlag == KBibTeX::TypeFlag::Reference && !encodedText.contains(invalidCharsForReferenceRegExp)) { value.append(QSharedPointer<MacroKey>(new MacroKey(encodedText))); return true; - } else if (typeFlag == KBibTeX::tfPerson) { + } else if (typeFlag == KBibTeX::TypeFlag::Person) { QSharedPointer<Person> person = FileImporterBibTeX::personFromString(encodedText); if (!person.isNull()) value.append(person); return true; - } else if (typeFlag == KBibTeX::tfKeyword) { + } else if (typeFlag == KBibTeX::TypeFlag::Keyword) { const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(encodedText); for (const auto &keyword : keywords) value.append(keyword); return true; - } else if (typeFlag == KBibTeX::tfSource) { - const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); + } else if (typeFlag == KBibTeX::TypeFlag::Source) { + const QString key = typeFlags.testFlag(KBibTeX::TypeFlag::Person) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (!file.isNull() && file->count() == 1) { QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); if (!entry.isNull()) { value = entry->value(key); return !value.isEmpty(); } else qCWarning(LOG_KBIBTEX_GUI) << "Parsing " << fakeBibTeXFile << " did not result in valid entry"; } - } else if (typeFlag == KBibTeX::tfVerbatim) { + } else if (typeFlag == KBibTeX::TypeFlag::Verbatim) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); return true; } return false; } bool validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified - const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); + const QString text = typeFlag == KBibTeX::TypeFlag::Source || typeFlag == KBibTeX::TypeFlag::Verbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); if (encodedText.isEmpty()) return true; bool result = false; - if (typeFlag == KBibTeX::tfPlainText || typeFlag == KBibTeX::tfPerson || typeFlag == KBibTeX::tfKeyword) { + if (typeFlag == KBibTeX::TypeFlag::PlainText || typeFlag == KBibTeX::TypeFlag::Person || typeFlag == KBibTeX::TypeFlag::Keyword) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); - } else if (typeFlag == KBibTeX::tfReference) { + } else if (typeFlag == KBibTeX::TypeFlag::Reference) { static const QRegularExpression validReferenceRegExp(QStringLiteral("^[-_:/a-zA-Z0-9]+$")); const QRegularExpressionMatch validReferenceMatch = validReferenceRegExp.match(text); result = validReferenceMatch.hasMatch() && validReferenceMatch.captured() == text; if (!result) message = i18n("Reference contains characters outside of the allowed set."); - } else if (typeFlag == KBibTeX::tfSource) { - const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); + } else if (typeFlag == KBibTeX::TypeFlag::Source) { + const QString key = typeFlags.testFlag(KBibTeX::TypeFlag::Person) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (file.isNull() || file->count() != 1) return false; QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); result = !entry.isNull() && entry->count() == 1; if (!result) message = i18n("Source code could not be parsed correctly."); - } else if (typeFlag == KBibTeX::tfVerbatim) { + } else if (typeFlag == KBibTeX::TypeFlag::Verbatim) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = parent; return result; } KBibTeX::TypeFlag determineTypeFlag(const Value &value, KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags availableTypeFlags) { - KBibTeX::TypeFlag result = KBibTeX::tfSource; + KBibTeX::TypeFlag result = KBibTeX::TypeFlag::Source; if (availableTypeFlags.testFlag(preferredTypeFlag) && typeFlagSupported(value, preferredTypeFlag)) result = preferredTypeFlag; else if (value.count() == 1) { int p = 1; for (int i = 1; i < 8; ++i, p <<= 1) { const KBibTeX::TypeFlag flag = static_cast<KBibTeX::TypeFlag>(p); if (availableTypeFlags.testFlag(flag) && typeFlagSupported(value, flag)) { result = flag; break; } } } return result; } bool typeFlagSupported(const Value &value, KBibTeX::TypeFlag typeFlag) { - if (value.isEmpty() || typeFlag == KBibTeX::tfSource) + if (value.isEmpty() || typeFlag == KBibTeX::TypeFlag::Source) return true; const QSharedPointer<ValueItem> first = value.first(); if (value.count() > 1) - return typeFlag == KBibTeX::tfSource; - else if (typeFlag == KBibTeX::tfKeyword && Keyword::isKeyword(*first)) + return typeFlag == KBibTeX::TypeFlag::Source; + else if (typeFlag == KBibTeX::TypeFlag::Keyword && Keyword::isKeyword(*first)) return true; - else if (typeFlag == KBibTeX::tfPerson && Person::isPerson(*first)) + else if (typeFlag == KBibTeX::TypeFlag::Person && Person::isPerson(*first)) return true; - else if (typeFlag == KBibTeX::tfPlainText && PlainText::isPlainText(*first)) + else if (typeFlag == KBibTeX::TypeFlag::PlainText && PlainText::isPlainText(*first)) return true; - else if (typeFlag == KBibTeX::tfReference && MacroKey::isMacroKey(*first)) + else if (typeFlag == KBibTeX::TypeFlag::Reference && MacroKey::isMacroKey(*first)) return true; - else if (typeFlag == KBibTeX::tfVerbatim && VerbatimText::isVerbatimText(*first)) + else if (typeFlag == KBibTeX::TypeFlag::Verbatim && VerbatimText::isVerbatimText(*first)) return true; else return false; } void setupMenu() { menuTypes->clear(); - if (typeFlags.testFlag(KBibTeX::tfPlainText)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPlainText), i18n("Plain Text")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::PlainText)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::PlainText), i18n("Plain Text")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfPlainText); + typeChanged(KBibTeX::TypeFlag::PlainText); }); } - if (typeFlags.testFlag(KBibTeX::tfReference)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfReference), i18n("Reference")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::Reference)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::Reference), i18n("Reference")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfReference); + typeChanged(KBibTeX::TypeFlag::Reference); }); } - if (typeFlags.testFlag(KBibTeX::tfPerson)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPerson), i18n("Person")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::Person)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::Person), i18n("Person")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfPerson); + typeChanged(KBibTeX::TypeFlag::Person); }); } - if (typeFlags.testFlag(KBibTeX::tfKeyword)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfKeyword), i18n("Keyword")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::Keyword)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::Keyword), i18n("Keyword")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfKeyword); + typeChanged(KBibTeX::TypeFlag::Keyword); }); } - if (typeFlags.testFlag(KBibTeX::tfSource)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfSource), i18n("Source Code")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::Source)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::Source), i18n("Source Code")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfSource); + typeChanged(KBibTeX::TypeFlag::Source); }); } - if (typeFlags.testFlag(KBibTeX::tfVerbatim)) { - QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfVerbatim), i18n("Verbatim Text")); + if (typeFlags.testFlag(KBibTeX::TypeFlag::Verbatim)) { + QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::TypeFlag::Verbatim), i18n("Verbatim Text")); connect(action, &QAction::triggered, parent, [this]() { - typeChanged(KBibTeX::tfVerbatim); + typeChanged(KBibTeX::TypeFlag::Verbatim); }); } } QIcon iconForTypeFlag(KBibTeX::TypeFlag typeFlag) { switch (typeFlag) { - case KBibTeX::tfInvalid: return QIcon(); - case KBibTeX::tfPlainText: return QIcon::fromTheme(QStringLiteral("draw-text")); - case KBibTeX::tfReference: return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); - case KBibTeX::tfPerson: return QIcon::fromTheme(QStringLiteral("user-identity")); - case KBibTeX::tfKeyword: return QIcon::fromTheme(QStringLiteral("edit-find")); - case KBibTeX::tfSource: return QIcon::fromTheme(QStringLiteral("code-context")); - case KBibTeX::tfVerbatim: return QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard")); + case KBibTeX::TypeFlag::Invalid: return QIcon(); + case KBibTeX::TypeFlag::PlainText: return QIcon::fromTheme(QStringLiteral("draw-text")); + case KBibTeX::TypeFlag::Reference: return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); + case KBibTeX::TypeFlag::Person: return QIcon::fromTheme(QStringLiteral("user-identity")); + case KBibTeX::TypeFlag::Keyword: return QIcon::fromTheme(QStringLiteral("edit-find")); + case KBibTeX::TypeFlag::Source: return QIcon::fromTheme(QStringLiteral("code-context")); + case KBibTeX::TypeFlag::Verbatim: return QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard")); } return QIcon(); //< should never happen as switch above covers all cases } void updateGUI(KBibTeX::TypeFlag typeFlag) { parent->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); parent->setIcon(iconForTypeFlag(typeFlag)); switch (typeFlag) { - case KBibTeX::tfInvalid: parent->setButtonToolTip(QString()); break; - case KBibTeX::tfPlainText: parent->setButtonToolTip(i18n("Plain Text")); break; - case KBibTeX::tfReference: parent->setButtonToolTip(i18n("Reference")); break; - case KBibTeX::tfPerson: parent->setButtonToolTip(i18n("Person")); break; - case KBibTeX::tfKeyword: parent->setButtonToolTip(i18n("Keyword")); break; - case KBibTeX::tfSource: + case KBibTeX::TypeFlag::Invalid: parent->setButtonToolTip(QString()); break; + case KBibTeX::TypeFlag::PlainText: parent->setButtonToolTip(i18n("Plain Text")); break; + case KBibTeX::TypeFlag::Reference: parent->setButtonToolTip(i18n("Reference")); break; + case KBibTeX::TypeFlag::Person: parent->setButtonToolTip(i18n("Person")); break; + case KBibTeX::TypeFlag::Keyword: parent->setButtonToolTip(i18n("Keyword")); break; + case KBibTeX::TypeFlag::Source: parent->setButtonToolTip(i18n("Source Code")); parent->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); break; - case KBibTeX::tfVerbatim: parent->setButtonToolTip(i18n("Verbatim Text")); break; + case KBibTeX::TypeFlag::Verbatim: parent->setButtonToolTip(i18n("Verbatim Text")); break; } } void openUrl() { if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, parent, KRun::RunFlags()); } } bool convertValueType(Value &value, KBibTeX::TypeFlag destType) { if (value.isEmpty()) return true; /// simple case - if (destType == KBibTeX::tfSource) return true; /// simple case + if (destType == KBibTeX::TypeFlag::Source) return true; /// simple case bool result = true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); QString rawText; const QSharedPointer<ValueItem> first = value.first(); const QSharedPointer<PlainText> plainText = first.dynamicCast<PlainText>(); if (!plainText.isNull()) - rawText = encoder.encode(plainText->text(), Encoder::TargetEncodingASCII); + rawText = encoder.encode(plainText->text(), Encoder::TargetEncoding::ASCII); else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); if (!verbatimText.isNull()) rawText = verbatimText->text(); else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); if (!macroKey.isNull()) rawText = macroKey->text(); else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); if (!person.isNull()) - rawText = encoder.encode(QString(QStringLiteral("%1 %2")).arg(person->firstName(), person->lastName()), Encoder::TargetEncodingASCII); // FIXME proper name conversion + rawText = encoder.encode(QString(QStringLiteral("%1 %2")).arg(person->firstName(), person->lastName()), Encoder::TargetEncoding::ASCII); // FIXME proper name conversion else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); if (!keyword.isNull()) - rawText = encoder.encode(keyword->text(), Encoder::TargetEncodingASCII); + rawText = encoder.encode(keyword->text(), Encoder::TargetEncoding::ASCII); else { // TODO case missed? result = false; } } } } } switch (destType) { - case KBibTeX::tfPlainText: + case KBibTeX::TypeFlag::PlainText: value.clear(); value.append(QSharedPointer<PlainText>(new PlainText(encoder.decode(rawText)))); break; - case KBibTeX::tfVerbatim: + case KBibTeX::TypeFlag::Verbatim: value.clear(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); break; - case KBibTeX::tfPerson: + case KBibTeX::TypeFlag::Person: value.clear(); value.append(QSharedPointer<Person>(FileImporterBibTeX::splitName(encoder.decode(rawText)))); break; - case KBibTeX::tfReference: { + case KBibTeX::TypeFlag::Reference: { MacroKey *macroKey = new MacroKey(rawText); if (macroKey->isValid()) { value.clear(); value.append(QSharedPointer<MacroKey>(macroKey)); } else { delete macroKey; result = false; } } break; - case KBibTeX::tfKeyword: + case KBibTeX::TypeFlag::Keyword: value.clear(); value.append(QSharedPointer<Keyword>(new Keyword(encoder.decode(rawText)))); break; default: { // TODO result = false; } } return result; } void updateURL(const QString &text) { QSet<QUrl> urls; - FileInfo::urlsInText(text, FileInfo::TestExistenceYes, file != nullptr && file->property(File::Url).toUrl().isValid() ? QUrl(file->property(File::Url).toUrl()).path() : QString(), urls); + FileInfo::urlsInText(text, FileInfo::TestExistence::Yes, file != nullptr && file->property(File::Url).toUrl().isValid() ? QUrl(file->property(File::Url).toUrl()).path() : QString(), urls); QSet<QUrl>::ConstIterator urlsIt = urls.constBegin(); if (urlsIt != urls.constEnd() && (*urlsIt).isValid()) urlToOpen = (*urlsIt); else urlToOpen = QUrl(); /// set special "open URL" button visible if URL (or file or DOI) found buttonOpenUrl->setVisible(urlToOpen.isValid()); buttonOpenUrl->setToolTip(i18n("Open '%1'", urlToOpen.url(QUrl::PreferLocalFile))); } void textChanged(const QString &text) { updateURL(text); } void typeChanged(const KBibTeX::TypeFlag newTypeFlag) { Value value; apply(value); if (convertValueType(value, newTypeFlag)) { typeFlag = newTypeFlag; reset(value); } else KMessageBox::error(parent, i18n("The current text cannot be used as value of type '%1'.\n\nSwitching back to type '%2'.", BibTeXFields::typeFlagToString(newTypeFlag), BibTeXFields::typeFlagToString(typeFlag))); } }; FieldLineEdit::FieldLineEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, bool isMultiLine, QWidget *parent) : MenuLineEdit(isMultiLine, parent), d(new FieldLineEdit::FieldLineEditPrivate(preferredTypeFlag, typeFlags, this)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); setObjectName(QStringLiteral("FieldLineEdit")); setMenu(d->menuTypes); setChildAcceptDrops(false); setAcceptDrops(true); } FieldLineEdit::~FieldLineEdit() { delete d; } bool FieldLineEdit::apply(Value &value) const { return d->apply(value); } bool FieldLineEdit::reset(const Value &value) { return d->reset(value); } bool FieldLineEdit::validate(QWidget **widgetWithIssue, QString &message) const { return d->validate(widgetWithIssue, message); } void FieldLineEdit::setReadOnly(bool isReadOnly) { MenuLineEdit::setReadOnly(isReadOnly); } void FieldLineEdit::setFile(const File *file) { d->file = file; } void FieldLineEdit::setElement(const Element *element) { Q_UNUSED(element) } void FieldLineEdit::setFieldKey(const QString &fieldKey) { d->fieldKey = fieldKey; } void FieldLineEdit::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex"))) event->acceptProposedAction(); } void FieldLineEdit::dropEvent(QDropEvent *event) { const QString clipboardText = event->mimeData()->text(); if (clipboardText.isEmpty()) return; bool success = false; if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) { FileImporterBibTeX importer(this); QScopedPointer<File> file(importer.fromString(clipboardText)); const QSharedPointer<Entry> entry = (!file.isNull() && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>(); if (!entry.isNull() && d->fieldKey == Entry::ftCrossRef) { /// handle drop on crossref line differently (use dropped entry's id) Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(entry->id()))); reset(v); emit textChanged(entry->id()); success = true; } else if (!entry.isNull() && entry->contains(d->fieldKey)) { /// case for "normal" fields like for journal, pages, ... reset(entry->value(d->fieldKey)); emit textChanged(text()); success = true; } } if (!success) { /// fall-back case: just copy whole text into edit widget setText(clipboardText); emit textChanged(clipboardText); } } diff --git a/src/gui/field/fieldlistedit.cpp b/src/gui/field/fieldlistedit.cpp index 908384a3..0759e32e 100644 --- a/src/gui/field/fieldlistedit.cpp +++ b/src/gui/field/fieldlistedit.cpp @@ -1,719 +1,719 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fieldlistedit.h" #include <typeinfo> #include <QApplication> #include <QClipboard> #include <QScrollArea> #include <QLayout> #include <QCheckBox> #include <QDragEnterEvent> #include <QDropEvent> #include <QMimeData> #include <QUrl> #include <QTimer> #include <QAction> #include <QPushButton> #include <QFontDatabase> #include <QFileDialog> #include <QMenu> #include <KMessageBox> #include <KLocalizedString> #include <KIO/CopyJob> #include <KSharedConfig> #include <KConfigGroup> #include <File> #include <Entry> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <FileInfo> #include <AssociatedFiles> #include "fieldlineedit.h" #include "element/associatedfilesui.h" #include "logging_gui.h" class FieldListEdit::FieldListEditProtected { private: FieldListEdit *p; const int innerSpacing; QVBoxLayout *layout; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; public: QList<FieldLineEdit *> lineEditList; QWidget *pushButtonContainer; QBoxLayout *pushButtonContainerLayout; QPushButton *addLineButton; const File *file; QString fieldKey; QWidget *container; QScrollArea *scrollArea; bool m_isReadOnly; QStringList completionItems; FieldListEditProtected(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldListEdit *parent) : p(parent), innerSpacing(4), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr), m_isReadOnly(false) { setupGUI(); } FieldListEditProtected(const FieldListEditProtected &other) = delete; FieldListEditProtected &operator= (const FieldListEditProtected &other) = delete; void setupGUI() { QBoxLayout *outerLayout = new QVBoxLayout(p); outerLayout->setMargin(0); outerLayout->setSpacing(0); scrollArea = new QScrollArea(p); outerLayout->addWidget(scrollArea); container = new QWidget(scrollArea->viewport()); container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); scrollArea->setWidget(container); layout = new QVBoxLayout(container); layout->setMargin(0); layout->setSpacing(innerSpacing); pushButtonContainer = new QWidget(container); pushButtonContainerLayout = new QHBoxLayout(pushButtonContainer); pushButtonContainerLayout->setMargin(0); layout->addWidget(pushButtonContainer); addLineButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), pushButtonContainer); addLineButton->setObjectName(QStringLiteral("addButton")); connect(addLineButton, &QPushButton::clicked, p, static_cast<void(FieldListEdit::*)()>(&FieldListEdit::lineAdd)); connect(addLineButton, &QPushButton::clicked, p, &FieldListEdit::modified); pushButtonContainerLayout->addWidget(addLineButton); layout->addStretch(100); scrollArea->setBackgroundRole(QPalette::Base); scrollArea->ensureWidgetVisible(container); scrollArea->setWidgetResizable(true); } void addButton(QPushButton *button) { button->setParent(pushButtonContainer); pushButtonContainerLayout->addWidget(button); } int recommendedHeight() { int heightHint = 0; for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(lineEditList)) heightHint += fieldLineEdit->sizeHint().height(); heightHint += lineEditList.count() * innerSpacing; heightHint += addLineButton->sizeHint().height(); return heightHint; } FieldLineEdit *addFieldLineEdit() { FieldLineEdit *le = new FieldLineEdit(preferredTypeFlag, typeFlags, false, container); le->setFile(file); le->setAcceptDrops(false); le->setReadOnly(m_isReadOnly); le->setInnerWidgetsTransparency(true); layout->insertWidget(layout->count() - 2, le); lineEditList.append(le); QPushButton *remove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), le); remove->setToolTip(i18n("Remove value")); le->appendWidget(remove); connect(remove, &QPushButton::clicked, p, [this, le]() { removeFieldLineEdit(le); const QSize size(container->width(), recommendedHeight()); container->resize(size); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); }); QPushButton *goDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), le); goDown->setToolTip(i18n("Move value down")); le->appendWidget(goDown); connect(goDown, &QPushButton::clicked, p, [this, le]() { const bool gotModified = goDownFieldLineEdit(le); if (gotModified) { /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }); QPushButton *goUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), le); goUp->setToolTip(i18n("Move value up")); le->appendWidget(goUp); connect(goUp, &QPushButton::clicked, p, [this, le]() { const bool gotModified = goUpFieldLineEdit(le); if (gotModified) { /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }); connect(le, &FieldLineEdit::textChanged, p, &FieldListEdit::modified); return le; } void removeAllFieldLineEdits() { while (!lineEditList.isEmpty()) { FieldLineEdit *fieldLineEdit = *lineEditList.begin(); layout->removeWidget(fieldLineEdit); lineEditList.removeFirst(); delete fieldLineEdit; } /// This fixes a layout problem where the container element /// does not shrink correctly once the line edits have been /// removed QSize pSize = container->size(); pSize.setHeight(addLineButton->height()); container->resize(pSize); } void removeFieldLineEdit(FieldLineEdit *fieldLineEdit) { lineEditList.removeOne(fieldLineEdit); layout->removeWidget(fieldLineEdit); delete fieldLineEdit; } bool goDownFieldLineEdit(FieldLineEdit *fieldLineEdit) { int idx = lineEditList.indexOf(fieldLineEdit); if (idx < lineEditList.count() - 1) { layout->removeWidget(fieldLineEdit); lineEditList.removeOne(fieldLineEdit); lineEditList.insert(idx + 1, fieldLineEdit); layout->insertWidget(idx + 1, fieldLineEdit); return true; ///< return 'true' upon actual modification } return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at bottom } bool goUpFieldLineEdit(FieldLineEdit *fieldLineEdit) { int idx = lineEditList.indexOf(fieldLineEdit); if (idx > 0) { layout->removeWidget(fieldLineEdit); lineEditList.removeOne(fieldLineEdit); lineEditList.insert(idx - 1, fieldLineEdit); layout->insertWidget(idx - 1, fieldLineEdit); return true; ///< return 'true' upon actual modification } return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at top } }; FieldListEdit::FieldListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : QWidget(parent), d(new FieldListEditProtected(preferredTypeFlag, typeFlags, this)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumSize(fontMetrics().averageCharWidth() * 30, fontMetrics().averageCharWidth() * 10); setAcceptDrops(true); } FieldListEdit::~FieldListEdit() { delete d; } bool FieldListEdit::reset(const Value &value) { d->removeAllFieldLineEdits(); for (const auto &valueItem : value) { Value v; v.append(valueItem); FieldLineEdit *fieldLineEdit = addFieldLineEdit(); fieldLineEdit->setFile(d->file); fieldLineEdit->reset(v); } QSize size(d->container->width(), d->recommendedHeight()); d->container->resize(size); return true; } bool FieldListEdit::apply(Value &value) const { value.clear(); for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) { Value v; fieldLineEdit->apply(v); for (const auto &valueItem : const_cast<const Value &>(v)) value.append(valueItem); } return true; } bool FieldListEdit::validate(QWidget **widgetWithIssue, QString &message) const { for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) { const bool v = fieldLineEdit->validate(widgetWithIssue, message); if (!v) return false; } return true; } void FieldListEdit::clear() { d->removeAllFieldLineEdits(); } void FieldListEdit::setReadOnly(bool isReadOnly) { d->m_isReadOnly = isReadOnly; for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) fieldLineEdit->setReadOnly(isReadOnly); d->addLineButton->setEnabled(!isReadOnly); } void FieldListEdit::setFile(const File *file) { d->file = file; for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) fieldLineEdit->setFile(file); } void FieldListEdit::setElement(const Element *element) { m_element = element; for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) fieldLineEdit->setElement(element); } void FieldListEdit::setFieldKey(const QString &fieldKey) { d->fieldKey = fieldKey; for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) fieldLineEdit->setFieldKey(fieldKey); } void FieldListEdit::setCompletionItems(const QStringList &items) { d->completionItems = items; for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) fieldLineEdit->setCompletionItems(items); } FieldLineEdit *FieldListEdit::addFieldLineEdit() { return d->addFieldLineEdit(); } void FieldListEdit::addButton(QPushButton *button) { d->addButton(button); } void FieldListEdit::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex"))) event->acceptProposedAction(); } void FieldListEdit::dropEvent(QDropEvent *event) { const QString clipboardText = event->mimeData()->text(); if (clipboardText.isEmpty()) return; const File *file = nullptr; if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) { FileImporterBibTeX importer(this); file = importer.fromString(clipboardText); const QSharedPointer<Entry> entry = (file != nullptr && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>(); if (file != nullptr && !entry.isNull() && d->fieldKey == QStringLiteral("^external")) { /// handle "external" list differently - const auto urlList = FileInfo::entryUrls(entry, QUrl(file->property(File::Url).toUrl()), FileInfo::TestExistenceNo); + const auto urlList = FileInfo::entryUrls(entry, QUrl(file->property(File::Url).toUrl()), FileInfo::TestExistence::No); Value v; v.reserve(urlList.size()); for (const QUrl &url : urlList) { v.append(QSharedPointer<VerbatimText>(new VerbatimText(url.url(QUrl::PreferLocalFile)))); } reset(v); emit modified(); return; } else if (!entry.isNull() && entry->contains(d->fieldKey)) { /// case for "normal" lists like for authors, editors, ... reset(entry->value(d->fieldKey)); emit modified(); return; } } if (file == nullptr || file->count() == 0) { /// fall-back case: single field line edit with text d->removeAllFieldLineEdits(); FieldLineEdit *fle = addFieldLineEdit(); fle->setText(clipboardText); emit modified(); } } void FieldListEdit::lineAdd(Value *value) { FieldLineEdit *le = addFieldLineEdit(); le->setCompletionItems(d->completionItems); if (value != nullptr) le->reset(*value); } void FieldListEdit::lineAdd() { FieldLineEdit *newEdit = addFieldLineEdit(); newEdit->setCompletionItems(d->completionItems); QSize size(d->container->width(), d->recommendedHeight()); d->container->resize(size); newEdit->setFocus(Qt::ShortcutFocusReason); } PersonListEdit::PersonListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : FieldListEdit(preferredTypeFlag, typeFlags, parent) { m_checkBoxOthers = new QCheckBox(i18n("... and others (et al.)"), this); connect(m_checkBoxOthers, &QCheckBox::toggled, this, &PersonListEdit::modified); QBoxLayout *boxLayout = static_cast<QBoxLayout *>(layout()); boxLayout->addWidget(m_checkBoxOthers); m_buttonAddNamesFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add from Clipboard"), this); m_buttonAddNamesFromClipboard->setToolTip(i18n("Add a list of names from clipboard")); addButton(m_buttonAddNamesFromClipboard); connect(m_buttonAddNamesFromClipboard, &QPushButton::clicked, this, &PersonListEdit::slotAddNamesFromClipboard); } bool PersonListEdit::reset(const Value &value) { Value internal = value; m_checkBoxOthers->setCheckState(Qt::Unchecked); QSharedPointer<PlainText> pt; if (!internal.isEmpty() && !(pt = internal.last().dynamicCast<PlainText>()).isNull()) { if (pt->text() == QStringLiteral("others")) { internal.erase(internal.end() - 1); m_checkBoxOthers->setCheckState(Qt::Checked); } } return FieldListEdit::reset(internal); } bool PersonListEdit::apply(Value &value) const { bool result = FieldListEdit::apply(value); if (result && m_checkBoxOthers->checkState() == Qt::Checked) value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others")))); return result; } void PersonListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_checkBoxOthers->setEnabled(!isReadOnly); m_buttonAddNamesFromClipboard->setEnabled(!isReadOnly); } void PersonListEdit::slotAddNamesFromClipboard() { QClipboard *clipboard = QApplication::clipboard(); QString text = clipboard->text(QClipboard::Clipboard); if (text.isEmpty()) text = clipboard->text(QClipboard::Selection); if (!text.isEmpty()) { const QList<QSharedPointer<Person> > personList = FileImporterBibTeX::splitNames(text); for (const QSharedPointer<Person> &person : personList) { Value *value = new Value(); value->append(person); lineAdd(value); delete value; } if (!personList.isEmpty()) emit modified(); } } UrlListEdit::UrlListEdit(QWidget *parent) - : FieldListEdit(KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, parent) + : FieldListEdit(KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, parent) { m_buttonAddFile = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add file..."), this); addButton(m_buttonAddFile); QMenu *menuAddFile = new QMenu(m_buttonAddFile); m_buttonAddFile->setMenu(menuAddFile); connect(m_buttonAddFile, &QPushButton::clicked, m_buttonAddFile, &QPushButton::showMenu); menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference ..."), this, SLOT(slotAddReference())); menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference from clipboard"), this, SLOT(slotAddReferenceFromClipboard())); } void UrlListEdit::slotAddReference() { QUrl bibtexUrl(d->file != nullptr ? d->file->property(File::Url, QVariant()).toUrl() : QUrl()); if (bibtexUrl.isValid()) { const QFileInfo fi(bibtexUrl.path()); bibtexUrl.setPath(fi.absolutePath()); } const QUrl documentUrl = QFileDialog::getOpenFileUrl(this, i18n("File to Associate"), bibtexUrl); if (documentUrl.isValid()) addReference(documentUrl); } void UrlListEdit::slotAddReferenceFromClipboard() { const QUrl url = QUrl::fromUserInput(QApplication::clipboard()->text()); if (url.isValid()) addReference(url); } void UrlListEdit::addReference(const QUrl &url) { const Entry *entry = dynamic_cast<const Entry *>(m_element); if (entry != nullptr) { QSharedPointer<Entry> fakeTempEntry(new Entry(entry->type(), entry->id())); const QString visibleFilename = AssociatedFilesUI::associateUrl(url, fakeTempEntry, d->file, false, this); if (!visibleFilename.isEmpty()) { Value *value = new Value(); value->append(QSharedPointer<VerbatimText>(new VerbatimText(visibleFilename))); lineAdd(value); delete value; emit modified(); } } } void UrlListEdit::downloadAndSaveLocally(const QUrl &url) { /// Only proceed if Url is valid and points to a remote location if (url.isValid() && !url.isLocalFile()) { /// Get filename from url (without any path/directory part) QString filename = url.fileName(); /// Build QFileInfo from current BibTeX file if available QFileInfo bibFileinfo = d->file != nullptr ? QFileInfo(d->file->property(File::Url).toUrl().path()) : QFileInfo(); /// Build proposal to a local filename for remote file filename = bibFileinfo.isFile() ? bibFileinfo.absolutePath() + QDir::separator() + filename : filename; /// Ask user for actual local filename to save remote file to filename = QFileDialog::getSaveFileName(this, i18n("Save file locally"), filename, QStringLiteral("application/pdf application/postscript image/vnd.djvu")); /// Check if user entered a valid filename ... if (!filename.isEmpty()) { /// Ask user if reference to local file should be /// relative or absolute in relation to the BibTeX file const QString absoluteFilename = filename; QString visibleFilename = filename; if (bibFileinfo.isFile()) visibleFilename = askRelativeOrStaticFilename(this, absoluteFilename, d->file->property(File::Url).toUrl()); /// Download remote file and save it locally setEnabled(false); setCursor(Qt::WaitCursor); KIO::CopyJob *job = KIO::copy(url, QUrl::fromLocalFile(absoluteFilename), KIO::Overwrite); job->setProperty("visibleFilename", QVariant::fromValue<QString>(visibleFilename)); connect(job, &KJob::result, this, &UrlListEdit::downloadFinished); } } } void UrlListEdit::downloadFinished(KJob *j) { KIO::CopyJob *job = static_cast<KIO::CopyJob *>(j); if (job->error() == 0) { /// Download succeeded, add reference to local file to this BibTeX entry Value *value = new Value(); value->append(QSharedPointer<VerbatimText>(new VerbatimText(job->property("visibleFilename").toString()))); lineAdd(value); delete value; } else { qCWarning(LOG_KBIBTEX_GUI) << "Downloading" << (*job->srcUrls().constBegin()).toDisplayString() << "failed with error" << job->error() << job->errorString(); } setEnabled(true); unsetCursor(); } void UrlListEdit::textChanged(QPushButton *buttonSaveLocally, FieldLineEdit *fieldLineEdit) { if (buttonSaveLocally == nullptr || fieldLineEdit == nullptr) return; ///< should never happen! /// Create URL from new text to make some tests on it /// Only remote URLs are of interest, therefore no tests /// on local file or relative paths const QString newText = fieldLineEdit->text(); const QString lowerText = newText.toLower(); /// Enable button only if Url is valid and points to a remote /// DjVu, PDF, or PostScript file // TODO more file types? const bool canBeSaved = lowerText.contains(QStringLiteral("://")) && (lowerText.endsWith(QStringLiteral(".djvu")) || lowerText.endsWith(QStringLiteral(".pdf")) || lowerText.endsWith(QStringLiteral(".ps"))); buttonSaveLocally->setEnabled(canBeSaved); buttonSaveLocally->setToolTip(canBeSaved ? i18n("Save file '%1' locally", newText) : QString()); } QString UrlListEdit::askRelativeOrStaticFilename(QWidget *parent, const QString &absoluteFilename, const QUrl &baseUrl) { QFileInfo baseUrlInfo = baseUrl.isValid() ? QFileInfo(baseUrl.path()) : QFileInfo(); QFileInfo filenameInfo(absoluteFilename); if (baseUrl.isValid() && (filenameInfo.absolutePath() == baseUrlInfo.absolutePath() || filenameInfo.absolutePath().startsWith(baseUrlInfo.absolutePath() + QDir::separator()))) { // TODO cover level-up cases like "../../test.pdf" const QString relativePath = filenameInfo.absolutePath().mid(baseUrlInfo.absolutePath().length() + 1); const QString relativeFilename = relativePath + (relativePath.isEmpty() ? QString() : QString(QDir::separator())) + filenameInfo.fileName(); if (KMessageBox::questionYesNo(parent, i18n("<qt><p>Use a filename relative to the bibliography file?</p><p>The relative path would be<br/><tt style=\"font-family: %3;\">%1</tt></p><p>The absolute path would be<br/><tt style=\"font-family: %3;\">%2</tt></p></qt>", relativeFilename, absoluteFilename, QFontDatabase::systemFont(QFontDatabase::FixedFont).family()), i18n("Relative Path"), KGuiItem(i18n("Relative Path")), KGuiItem(i18n("Absolute Path"))) == KMessageBox::Yes) return relativeFilename; } return absoluteFilename; } FieldLineEdit *UrlListEdit::addFieldLineEdit() { /// Call original implementation to get an instance of a FieldLineEdit FieldLineEdit *fieldLineEdit = FieldListEdit::addFieldLineEdit(); /// Create a new "save locally" button QPushButton *buttonSaveLocally = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), QString(), fieldLineEdit); buttonSaveLocally->setToolTip(i18n("Save file locally")); buttonSaveLocally->setEnabled(false); /// Append button to new FieldLineEdit fieldLineEdit->appendWidget(buttonSaveLocally); /// Connect signals to react on button events /// or changes in the FieldLineEdit's text connect(buttonSaveLocally, &QPushButton::clicked, this, [this, fieldLineEdit]() { downloadAndSaveLocally(QUrl::fromUserInput(fieldLineEdit->text())); }); connect(fieldLineEdit, &FieldLineEdit::textChanged, this, [this, buttonSaveLocally, fieldLineEdit]() { textChanged(buttonSaveLocally, fieldLineEdit); }); return fieldLineEdit; } void UrlListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_buttonAddFile->setEnabled(!isReadOnly); } const QString KeywordListEdit::keyGlobalKeywordList = QStringLiteral("globalKeywordList"); KeywordListEdit::KeywordListEdit(QWidget *parent) - : FieldListEdit(KBibTeX::tfKeyword, KBibTeX::tfKeyword | KBibTeX::tfSource, parent), m_config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), m_configGroupName(QStringLiteral("Global Keywords")) + : FieldListEdit(KBibTeX::TypeFlag::Keyword, KBibTeX::TypeFlag::Keyword | KBibTeX::TypeFlag::Source, parent), m_config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), m_configGroupName(QStringLiteral("Global Keywords")) { m_buttonAddKeywordsFromList = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Keywords from List"), this); m_buttonAddKeywordsFromList->setToolTip(i18n("Add keywords as selected from a pre-defined list of keywords")); addButton(m_buttonAddKeywordsFromList); connect(m_buttonAddKeywordsFromList, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromList); m_buttonAddKeywordsFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add Keywords from Clipboard"), this); m_buttonAddKeywordsFromClipboard->setToolTip(i18n("Add a punctuation-separated list of keywords from clipboard")); addButton(m_buttonAddKeywordsFromClipboard); connect(m_buttonAddKeywordsFromClipboard, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromClipboard); } void KeywordListEdit::slotAddKeywordsFromList() { /// fetch stored, global keywords KConfigGroup configGroup(m_config, m_configGroupName); QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList()); /// use a map for case-insensitive sorting of strings /// (recommended by Qt's documentation) QMap<QString, QString> forCaseInsensitiveSorting; /// insert all stored, global keywords for (const QString &keyword : const_cast<const QStringList &>(keywords)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// insert all unique keywords used in this file for (const QString &keyword : const_cast<const QSet<QString> &>(m_keywordsFromFile)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// re-create string list from map's values keywords = forCaseInsensitiveSorting.values(); // FIXME QInputDialog does not have a 'getItemList' /* bool ok = false; const QStringList newKeywordList = KInputDialog::getItemList(i18n("Add Keywords"), i18n("Select keywords to add:"), keywords, QStringList(), true, &ok, this); if (ok) { for(const QString &newKeywordText : newKeywordList) { Value *value = new Value(); value->append(QSharedPointer<Keyword>(new Keyword(newKeywordText))); lineAdd(value); delete value; } if (!newKeywordList.isEmpty()) emit modified(); } */ } void KeywordListEdit::slotAddKeywordsFromClipboard() { QClipboard *clipboard = QApplication::clipboard(); QString text = clipboard->text(QClipboard::Clipboard); if (text.isEmpty()) ///< use "mouse" clipboard as fallback text = clipboard->text(QClipboard::Selection); if (!text.isEmpty()) { const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(text); for (const auto &keyword : keywords) { Value *value = new Value(); value->append(keyword); lineAdd(value); delete value; } if (!keywords.isEmpty()) emit modified(); } } void KeywordListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_buttonAddKeywordsFromList->setEnabled(!isReadOnly); m_buttonAddKeywordsFromClipboard->setEnabled(!isReadOnly); } void KeywordListEdit::setFile(const File *file) { if (file == nullptr) m_keywordsFromFile.clear(); else m_keywordsFromFile = file->uniqueEntryValuesSet(Entry::ftKeywords); FieldListEdit::setFile(file); } void KeywordListEdit::setCompletionItems(const QStringList &items) { /// fetch stored, global keywords KConfigGroup configGroup(m_config, m_configGroupName); QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList()); /// use a map for case-insensitive sorting of strings /// (recommended by Qt's documentation) QMap<QString, QString> forCaseInsensitiveSorting; /// insert all stored, global keywords for (const QString &keyword : const_cast<const QStringList &>(keywords)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// insert all unique keywords used in this file for (const QString &keyword : const_cast<const QStringList &>(items)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// re-create string list from map's values keywords = forCaseInsensitiveSorting.values(); FieldListEdit::setCompletionItems(keywords); } diff --git a/src/gui/file/fileview.cpp b/src/gui/file/fileview.cpp index b6fea040..f97195b4 100644 --- a/src/gui/file/fileview.cpp +++ b/src/gui/file/fileview.cpp @@ -1,367 +1,367 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileview.h" #include <QDropEvent> #include <QTimer> #include <QDialog> #include <QDialogButtonBox> #include <QBoxLayout> #include <QPushButton> #include <KLocalizedString> #include <KMessageBox> #include <KGuiItem> #include <KConfigGroup> #include <KSharedConfig> #include <KWindowConfig> #include <KStandardGuiItem> #include <Entry> #include <Macro> #include <models/FileModel> #include <FileExporterBibTeX> #include "element/elementeditor.h" #include "valuelistmodel.h" #include "logging_gui.h" /** * Specialized dialog for element editing. It will check if the used * element editor widget has unapplied changes and ask the user if * he/she actually wants to discard those changes before closing this * dialog. * * @author Thomas Fischer */ class ElementEditorDialog : public QDialog { Q_OBJECT private: ElementEditor *elementEditor; static const QString configGroupNameWindowSize; KConfigGroup configGroup; public: ElementEditorDialog(QWidget *parent) : QDialog(parent), elementEditor(nullptr) { /// restore window size KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))); configGroup = KConfigGroup(config, configGroupNameWindowSize); KWindowConfig::restoreWindowSize(windowHandle(), configGroup); setLayout(new QVBoxLayout(parent)); } /** * Store element editor widget for future reference. */ void setElementEditor(ElementEditor *elementEditor) { this->elementEditor = elementEditor; QBoxLayout *boxLayout = qobject_cast<QBoxLayout *>(layout()); boxLayout->addWidget(this->elementEditor); } protected: void closeEvent(QCloseEvent *e) override { /// strangely enough, close events have always to be rejected ... e->setAccepted(false); QDialog::closeEvent(e); } private: bool allowedToClose() { /// save window size KWindowConfig::saveWindowSize(windowHandle(), configGroup); /// if there unapplied changes in the editor widget ... /// ... ask user for consent to discard changes ... /// only the allow to close this dialog return !elementEditor->elementUnapplied() || KMessageBox::warningContinueCancel(this, i18n("The current entry has been modified. Do you want do discard your changes?"), i18n("Discard changes?"), KStandardGuiItem::discard(), KGuiItem(i18n("Continue Editing"), QStringLiteral("edit-rename"))) == KMessageBox::Continue; } }; const QString ElementEditorDialog::configGroupNameWindowSize = QStringLiteral("ElementEditorDialog"); FileView::FileView(const QString &name, QWidget *parent) : BasicFileView(name, parent), m_isReadOnly(false), m_current(QSharedPointer<Element>()), m_filterBar(nullptr), m_lastEditorPage(nullptr), m_elementEditorDialog(nullptr), m_elementEditor(nullptr), m_dbb(nullptr) { connect(this, &FileView::doubleClicked, this, &FileView::itemActivated); } void FileView::viewCurrentElement() { viewElement(currentElement()); } void FileView::viewElement(const QSharedPointer<Element> element) { - prepareEditorDialog(DialogTypeView); + prepareEditorDialog(DialogType::View); FileModel *model = fileModel(); File *bibliographyFile = model != nullptr ? model->bibliographyFile() : nullptr; m_elementEditor->setElement(element, bibliographyFile); m_elementEditor->setCurrentPage(m_lastEditorPage); m_elementEditorDialog->exec(); m_lastEditorPage = m_elementEditor->currentPage(); } void FileView::editCurrentElement() { editElement(currentElement()); } bool FileView::editElement(QSharedPointer<Element> element) { - prepareEditorDialog(DialogTypeEdit); + prepareEditorDialog(DialogType::Edit); FileModel *model = fileModel(); File *bibliographyFile = model != nullptr ? model->bibliographyFile() : nullptr; m_elementEditor->setElement(element, bibliographyFile); m_elementEditor->setCurrentPage(m_lastEditorPage); m_elementEditorDialog->exec(); m_lastEditorPage = m_elementEditor->currentPage(); if (!isReadOnly()) { bool changed = m_elementEditor->elementChanged(); if (changed) { FileModel *model = fileModel(); const File *bibliographyFile = model != nullptr ? model->bibliographyFile() : nullptr; emit currentElementChanged(currentElement(), bibliographyFile); emit selectedElementsChanged(); emit modified(true); } return changed; } else return false; } const QList<QSharedPointer<Element> > &FileView::selectedElements() const { return m_selection; } void FileView::setSelectedElement(QSharedPointer<Element> element) { m_selection.clear(); m_selection << element; QItemSelectionModel *selModel = selectionModel(); selModel->clear(); FileModel *model = fileModel(); const int row = model != nullptr ? model->row(element) : -1; const QModelIndex sourceIdx = row >= 0 && model != nullptr ? model->index(row, 0) : QModelIndex(); const QModelIndex idx = sortFilterProxyModel()->mapFromSource(sourceIdx); selModel->setCurrentIndex(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); } const QSharedPointer<Element> FileView::currentElement() const { return m_current; } QSharedPointer<Element> FileView::currentElement() { return m_current; } QSharedPointer<Element> FileView::elementAt(const QModelIndex &index) { FileModel *model = fileModel(); return model != nullptr ? model->element(sortFilterProxyModel()->mapToSource(index).row()) : QSharedPointer<Element>(); } void FileView::currentChanged(const QModelIndex &current, const QModelIndex &previous) { QTreeView::currentChanged(current, previous); // FIXME necessary? m_current = elementAt(current); FileModel *model = fileModel(); if (model != nullptr) emit currentElementChanged(m_current, model->bibliographyFile()); } void FileView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QTreeView::selectionChanged(selected, deselected); const QModelIndexList selectedSet = selected.indexes(); for (const QModelIndex &index : selectedSet) { if (index.column() != 0) continue; ///< consider only column-0 indices to avoid duplicate elements m_selection.append(elementAt(index)); } if (m_current.isNull() && !selectedSet.isEmpty()) m_current = elementAt(selectedSet.first()); const QModelIndexList deselectedSet = deselected.indexes(); for (const QModelIndex &index : deselectedSet) { if (index.column() != 0) continue; ///< consider only column-0 indices to avoid duplicate elements m_selection.removeOne(elementAt(index)); } emit selectedElementsChanged(); } void FileView::selectionDelete() { const QModelIndexList mil = selectionModel()->selectedRows(); QList<int> rows; rows.reserve(mil.size()); for (const QModelIndex &idx : mil) rows << sortFilterProxyModel()->mapToSource(idx).row(); FileModel *model = fileModel(); if (model != nullptr) model->removeRowList(rows); emit modified(true); } /// FIXME the existence of this function is basically just one big hack void FileView::externalModification() { emit modified(true); } void FileView::setReadOnly(bool isReadOnly) { m_isReadOnly = isReadOnly; } bool FileView::isReadOnly() const { return m_isReadOnly; } ValueListModel *FileView::valueListModel(const QString &field) { FileModel *model = fileModel(); if (model != nullptr) { ValueListModel *result = new ValueListModel(model->bibliographyFile(), field, this); /// Keep track of external changes through modifications in this ValueListModel instance connect(result, &ValueListModel::dataChanged, this, &FileView::externalModification); return result; } return nullptr; } void FileView::setFilterBar(FilterBar *filterBar) { m_filterBar = filterBar; } void FileView::setFilterBarFilter(const SortFilterFileModel::FilterQuery &fq) { if (m_filterBar != nullptr) m_filterBar->setFilter(fq); } void FileView::mouseMoveEvent(QMouseEvent *event) { emit editorMouseEvent(event); } void FileView::dragEnterEvent(QDragEnterEvent *event) { emit editorDragEnterEvent(event); } void FileView::dropEvent(QDropEvent *event) { if (event->source() != this) emit editorDropEvent(event); } void FileView::dragMoveEvent(QDragMoveEvent *event) { emit editorDragMoveEvent(event); } void FileView::itemActivated(const QModelIndex &index) { emit elementExecuted(elementAt(index)); } void FileView::prepareEditorDialog(DialogType dialogType) { - if (dialogType != DialogTypeView && isReadOnly()) { + if (dialogType != DialogType::View && isReadOnly()) { qCWarning(LOG_KBIBTEX_GUI) << "In read-only mode, you may only view elements, not edit them"; - dialogType = DialogTypeView; + dialogType = DialogType::View; } /// Create both the dialog window and the editing widget only once if (m_elementEditorDialog == nullptr) m_elementEditorDialog = new ElementEditorDialog(this); if (m_elementEditor == nullptr) { m_elementEditor = new ElementEditor(false, m_elementEditorDialog); m_elementEditorDialog->setElementEditor(m_elementEditor); } if (m_dbb != nullptr) { delete m_dbb; m_dbb = nullptr; } - if (dialogType == DialogTypeView) { + if (dialogType == DialogType::View) { /// View mode, as use in read-only situations m_elementEditor->setReadOnly(true); m_elementEditorDialog->setWindowTitle(i18n("View Element")); m_dbb = new QDialogButtonBox(QDialogButtonBox::Close, m_elementEditorDialog); QBoxLayout *boxLayout = qobject_cast<QBoxLayout *>(m_elementEditorDialog->layout()); boxLayout->addWidget(m_dbb); connect(m_dbb, &QDialogButtonBox::clicked, this, &FileView::dialogButtonClicked); - } else if (dialogType == DialogTypeEdit) { + } else if (dialogType == DialogType::Edit) { /// Edit mode, used in normal operations m_elementEditor->setReadOnly(false); m_elementEditorDialog->setWindowTitle(i18n("Edit Element")); m_dbb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::Reset, m_elementEditorDialog); QBoxLayout *boxLayout = qobject_cast<QBoxLayout *>(m_elementEditorDialog->layout()); boxLayout->addWidget(m_dbb); m_dbb->button(QDialogButtonBox::Apply)->setEnabled(false); /// Establish signal-slot connections for modification/editing events connect(m_elementEditor, &ElementEditor::modified, m_dbb->button(QDialogButtonBox::Apply), &QPushButton::setEnabled); connect(m_dbb, &QDialogButtonBox::clicked, this, &FileView::dialogButtonClicked); } } void FileView::dialogButtonClicked(QAbstractButton *button) { switch (m_dbb->standardButton(button)) { case QDialogButtonBox::Ok: if (m_elementEditor->validate()) { m_elementEditor->apply(); m_elementEditorDialog->accept(); } break; case QDialogButtonBox::Apply: if (m_elementEditor->validate()) m_elementEditor->apply(); break; case QDialogButtonBox::Close: ///< fall-through is intentional case QDialogButtonBox::Cancel: m_elementEditorDialog->reject(); break; case QDialogButtonBox::Reset: m_elementEditor->reset(); break; default: qCWarning(LOG_KBIBTEX_GUI) << "Default case should never get triggered in FileView::dialogButtonClicked"; } } #include "fileview.moc" diff --git a/src/gui/file/fileview.h b/src/gui/file/fileview.h index aae7fc3a..5bff2217 100644 --- a/src/gui/file/fileview.h +++ b/src/gui/file/fileview.h @@ -1,108 +1,108 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_FILEVIEW_H #define KBIBTEX_GUI_FILEVIEW_H #include <QWidget> #include <widgets/FilterBar> #include <file/BasicFileView> #include <Element> #include "kbibtexgui_export.h" class QAbstractButton; class QDialogButtonBox; class ValueListModel; class ElementEditor; class ElementEditorDialog; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXGUI_EXPORT FileView : public BasicFileView { Q_OBJECT public: FileView(const QString &name, QWidget *parent); const QList<QSharedPointer<Element> > &selectedElements() const; const QSharedPointer<Element> currentElement() const; QSharedPointer<Element> currentElement(); QSharedPointer<Element> elementAt(const QModelIndex &index); void setReadOnly(bool isReadOnly = true); bool isReadOnly() const; ValueListModel *valueListModel(const QString &field); void setFilterBar(FilterBar *filterBar); signals: void selectedElementsChanged(); void currentElementChanged(QSharedPointer<Element>, const File *); void elementExecuted(QSharedPointer<Element>); void editorMouseEvent(QMouseEvent *); void editorDragEnterEvent(QDragEnterEvent *); void editorDragMoveEvent(QDragMoveEvent *); void editorDropEvent(QDropEvent *); void modified(bool); public slots: void viewCurrentElement(); void viewElement(const QSharedPointer<Element>); void editCurrentElement(); bool editElement(QSharedPointer<Element>); void setSelectedElement(QSharedPointer<Element>); void selectionDelete(); void externalModification(); void setFilterBarFilter(const SortFilterFileModel::FilterQuery &); protected: bool m_isReadOnly; void currentChanged(const QModelIndex &current, const QModelIndex &previous) override; void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; void mouseMoveEvent(QMouseEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dragMoveEvent(QDragMoveEvent *event) override; void dropEvent(QDropEvent *event) override; protected slots: void itemActivated(const QModelIndex &index); void dialogButtonClicked(QAbstractButton *); private: - enum DialogType { DialogTypeView, DialogTypeEdit }; + enum class DialogType { View, Edit }; QSharedPointer<Element> m_current; QList<QSharedPointer<Element> > m_selection; FilterBar *m_filterBar; QWidget *m_lastEditorPage; ElementEditorDialog *m_elementEditorDialog; ElementEditor *m_elementEditor; QDialogButtonBox *m_dbb; void prepareEditorDialog(DialogType dialogType); }; #endif // KBIBTEX_GUI_FILEVIEW_H diff --git a/src/gui/file/findduplicatesui.cpp b/src/gui/file/findduplicatesui.cpp index aaf6a519..a77ee984 100644 --- a/src/gui/file/findduplicatesui.cpp +++ b/src/gui/file/findduplicatesui.cpp @@ -1,741 +1,741 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "findduplicatesui.h" #include <QApplication> #include <QWidget> #include <QBoxLayout> #include <QLabel> #include <QLineEdit> #include <QSortFilterProxyModel> #include <QStyle> #include <QRadioButton> #include <QMouseEvent> #include <QKeyEvent> #include <QSplitter> #include <QPointer> #include <QStandardPaths> #include <QPushButton> #include <QAction> #include <QDialog> #include <QDialogButtonBox> #include <KActionCollection> #include <KLocalizedString> #include <kparts/part.h> #include <KMessageBox> #include <BibTeXEntries> #include <Entry> #include <models/FileModel> #include <FileImporterBibTeX> #include <FindDuplicates> #include "widgets/radiobuttontreeview.h" #include "fileview.h" #include "filedelegate.h" #include "logging_gui.h" /** * Model to hold alternative values as visualized in the RadioTreeView */ class AlternativesItemModel : public QAbstractItemModel { Q_OBJECT private: /// marker to memorize in an index's internal id that it is a top-level index static const quintptr noParentInternalId; /// parent widget, needed to get font from (for text in italics) QTreeView *p; EntryClique *currentClique; public: enum SelectionType {SelectionTypeNone, SelectionTypeRadio, SelectionTypeCheck}; enum AlternativesItemModelRole { /// Raw, all-lowercase field name FieldNameRole = Qt::UserRole + 101, /// Text used for inline editing of values UserInputRole = Qt::UserRole + 103 }; AlternativesItemModel(QTreeView *parent) : QAbstractItemModel(parent), p(parent), currentClique(nullptr) { /// nothing } static SelectionType selectionType(const QString &fieldName) { if (fieldName.isEmpty()) return SelectionTypeNone; if (fieldName == Entry::ftKeywords || fieldName == Entry::ftUrl) return SelectionTypeCheck; return SelectionTypeRadio; } void setCurrentClique(EntryClique *currentClique) { this->currentClique = currentClique; } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (parent == QModelIndex()) return createIndex(row, column, noParentInternalId); else if (parent.parent() == QModelIndex()) return createIndex(row, column, static_cast<quintptr>(parent.row())); return QModelIndex(); } QModelIndex parent(const QModelIndex &index) const override { if (index.internalId() >= noParentInternalId) return QModelIndex(); else return createIndex(static_cast<int>(index.internalId()), 0, noParentInternalId); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (currentClique == nullptr) return 0; if (parent == QModelIndex()) { /// top-level index, check how many lists of lists of alternatives exist return currentClique->fieldCount(); } else if (parent.parent() == QModelIndex()) { /// first, find the map of alternatives for this chosen field name (see parent) QString fieldName = parent.data(FieldNameRole).toString(); const auto alt = currentClique->values(fieldName); /// second, return number of alternatives for list of alternatives /// plus one for an "else" option return alt.count() + (fieldName.startsWith('^') || fieldName == Entry::ftKeywords || fieldName == Entry::ftUrl ? 0 : 1); } return 0; } int columnCount(const QModelIndex &) const override { /// only one column in use return 1; } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { Q_UNUSED(section) Q_UNUSED(orientation) if (role == Qt::DisplayRole) return i18n("Alternatives"); return QVariant(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (index.parent() == QModelIndex()) { /// top-level elements showing field names like "Title", "Authors", etc const QString fieldName = currentClique->fieldList().at(index.row()).toLower(); switch (role) { case AlternativesItemModelRole::FieldNameRole: /// plain-and-simple field name (all lower case) return fieldName; case Qt::ToolTipRole: case Qt::DisplayRole: /// nicely formatted field names for visual representation if (fieldName == QStringLiteral("^id")) return i18n("Identifier"); else if (fieldName == QStringLiteral("^type")) return i18n("Type"); else - return BibTeXEntries::instance().format(fieldName, KBibTeX::cUpperCamelCase); + return BibTeXEntries::instance().format(fieldName, KBibTeX::Casing::UpperCamelCase); case RadioButtonTreeView::IsRadioRole: /// this is not to be a radio widget return QVariant::fromValue(false); case Qt::FontRole: if (fieldName.startsWith('^')) { QFont f = p->font(); f.setItalic(true); return f; } else return p->font(); } } else if (index.parent().parent() == QModelIndex()) { /// second-level entries for alternatives /// start with determining which list of alternatives actually to use QString fieldName = index.parent().data(FieldNameRole).toString(); const auto &values = currentClique->values(fieldName); switch (role) { case Qt::EditRole: case Qt::ToolTipRole: case Qt::DisplayRole: if (index.row() < values.count()) { QString text = PlainTextValue::text(values.at(index.row())); if (fieldName == QStringLiteral("^type")) - text = BibTeXEntries::instance().format(text, KBibTeX::cUpperCamelCase); + text = BibTeXEntries::instance().format(text, KBibTeX::Casing::UpperCamelCase); /// textual representation of the alternative's value return text; } else /// add an "else" option return i18n("None of the above"); case Qt::FontRole: /// for the "else" option, make font italic if (index.row() >= values.count()) { QFont f = p->font(); f.setItalic(true); return f; } else return p->font(); case Qt::CheckStateRole: { if (selectionType(fieldName) != SelectionTypeCheck) return QVariant(); const auto chosenValues = currentClique->chosenValues(fieldName); QString text = PlainTextValue::text(values.at(index.row())); for (const Value &value : chosenValues) { if (PlainTextValue::text(value) == text) return Qt::Checked; } return Qt::Unchecked; } case RadioButtonTreeView::RadioSelectedRole: { if (selectionType(fieldName) != SelectionTypeRadio) return QVariant::fromValue(false); /// return selection status (true or false) for this alternative Value chosen = currentClique->chosenValue(fieldName); if (chosen.isEmpty()) return QVariant::fromValue(index.row() >= values.count()); else if (index.row() < values.count()) { QString chosenPlainText = PlainTextValue::text(chosen); QString rowPlainText = PlainTextValue::text(values[index.row()]); return QVariant::fromValue(chosenPlainText == rowPlainText); } return QVariant::fromValue(false); } case RadioButtonTreeView::IsRadioRole: /// this is to be a radio widget return QVariant::fromValue(selectionType(fieldName) == SelectionTypeRadio); } } return QVariant(); } bool setData(const QModelIndex &index, const QVariant &value, int role = RadioButtonTreeView::RadioSelectedRole) override { if (index.parent() != QModelIndex()) { bool isInt; int checkState = value.toInt(&isInt); const QString fieldName = index.parent().data(FieldNameRole).toString(); auto values = currentClique->values(fieldName); if (role == RadioButtonTreeView::RadioSelectedRole && value.canConvert<bool>() && value.toBool() == true && selectionType(fieldName) == SelectionTypeRadio) { /// start with determining which list of alternatives actually to use /// store which alternative was selected if (index.row() < values.count()) currentClique->setChosenValue(fieldName, values[index.row()]); else { Value v; currentClique->setChosenValue(fieldName, v); } /// update view on neighbouring alternatives emit dataChanged(index.sibling(0, 0), index.sibling(rowCount(index.parent()), 0)); return true; } else if (role == Qt::CheckStateRole && isInt && selectionType(fieldName) == SelectionTypeCheck) { if (checkState == Qt::Checked) - currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::AddValue); + currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::ValueOperation::AddValue); else if (checkState == Qt::Unchecked) - currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::RemoveValue); + currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::ValueOperation::RemoveValue); else return false; ///< tertium non datur emit dataChanged(index, index); return true; } else if (role == UserInputRole) { const QString text = value.toString(); if (text.isEmpty()) return false; const Value old = values.at(index.row()); if (old.isEmpty()) return false; Value v; QSharedPointer<PlainText> pt = old.first().dynamicCast<PlainText>(); if (!pt.isNull()) v.append(QSharedPointer<PlainText>(new PlainText(text))); else { QSharedPointer<VerbatimText> vt = old.first().dynamicCast<VerbatimText>(); if (!vt.isNull()) v.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); else { QSharedPointer<MacroKey> mk = old.first().dynamicCast<MacroKey>(); if (!mk.isNull()) v.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { QSharedPointer<Person> ps = old.first().dynamicCast<Person>(); if (!ps.isNull()) FileImporterBibTeX::parsePersonList(text, v); else { QSharedPointer<Keyword> kw = old.first().dynamicCast<Keyword>(); if (!kw.isNull()) { const QList<QSharedPointer<Keyword> > keywordList = FileImporterBibTeX::splitKeywords(text); v.reserve(keywordList.size()); for (const auto &keyword : keywordList) v.append(keyword); } else { qCDebug(LOG_KBIBTEX_GUI) << "Not know how to set this text:" << text; } } } } } if (!v.isEmpty()) { values.removeAt(index.row()); values.insert(index.row(), v); emit dataChanged(index, index); return true; } else return false; } } return false; } bool hasChildren(const QModelIndex &parent = QModelIndex()) const override { /// depth-two tree return parent == QModelIndex() || parent.parent() == QModelIndex(); } Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags f = QAbstractItemModel::flags(index); if (index.parent() != QModelIndex()) { QString fieldName = index.parent().data(FieldNameRole).toString(); if (selectionType(fieldName) == SelectionTypeCheck) f |= Qt::ItemIsUserCheckable; const auto values = currentClique->values(fieldName); if (index.row() < values.count()) f |= Qt::ItemIsEditable; } return f; } }; const quintptr AlternativesItemModel::noParentInternalId = std::numeric_limits<quintptr>::max(); /** * Specialization of RadioButtonItemDelegate which allows to edit * values in a AlternativesItemModel. */ class AlternativesItemDelegate: public RadioButtonItemDelegate { Q_OBJECT public: AlternativesItemDelegate(QObject *p) : RadioButtonItemDelegate(p) { /// nothing } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override { if (index.parent() != QModelIndex()) { /// Only second-level indices in the model can be edited /// (those are the actual values). /// Use a plain, border-less QLineEdit. QLineEdit *lineEdit = new QLineEdit(parent); lineEdit->setStyleSheet(QStringLiteral("border: none;")); return lineEdit; } return nullptr; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor)) { /// Set line edit's default value to string fetched from model lineEdit->setText(index.data(Qt::EditRole).toString()); } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor)) { /// Set user-entered text to model (and underlying value) model->setData(index, lineEdit->text(), AlternativesItemModel::UserInputRole); /// Ensure that the edited value is checked if it is /// a checkbox-checkable value, or gets a "dot" in its /// radio button if it is radio-checkable. const QString fieldName = index.parent().data(AlternativesItemModel::FieldNameRole).toString(); if (AlternativesItemModel::selectionType(fieldName) == AlternativesItemModel::SelectionTypeCheck) model->setData(index, Qt::Checked, Qt::CheckStateRole); else if (AlternativesItemModel::selectionType(fieldName) == AlternativesItemModel::SelectionTypeRadio) model->setData(index, true, RadioButtonTreeView::RadioSelectedRole); } } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override { QRect rect = option.rect; // TODO better placement of editing widget? //int radioButtonWidth = QApplication::style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth, &option); //int spacing = QApplication::style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing, &option); //rect.setLeft(rect.left() +spacing*3/2 + radioButtonWidth); editor->setGeometry(rect); } }; class CheckableFileModel : public FileModel { Q_OBJECT private: QVector<EntryClique *> cl; int currentClique; QTreeView *tv; public: CheckableFileModel(QVector<EntryClique *> &cliqueList, QTreeView *treeView, QObject *parent = nullptr) : FileModel(parent), cl(cliqueList), currentClique(0), tv(treeView) { /// nothing } void setCurrentClique(EntryClique *currentClique) { this->currentClique = cl.indexOf(currentClique); } QVariant data(const QModelIndex &index, int role) const override { if (role == Qt::CheckStateRole && index.column() == 1) { QSharedPointer<Entry> entry = element(index.row()).dynamicCast<Entry>(); Q_ASSERT_X(!entry.isNull(), "CheckableFileModel::data", "entry is NULL"); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = cl[currentClique]->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) // TODO does this work? return cl[currentClique]->isEntryChecked(QSharedPointer<Entry>(entry)) ? Qt::Checked : Qt::Unchecked; } } return FileModel::data(index, role); } bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override { bool ok; int checkState = value.toInt(&ok); Q_ASSERT_X(ok, "CheckableFileModel::setData", QString("Could not convert value " + value.toString()).toLatin1()); if (ok && role == Qt::CheckStateRole && index.column() == 1) { QSharedPointer<Entry> entry = element(index.row()).dynamicCast<Entry>(); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = cl[currentClique]->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) { // TODO does this work? EntryClique *ec = cl[currentClique]; ec->setEntryChecked(QSharedPointer<Entry>(entry), checkState == Qt::Checked); cl[currentClique] = ec; emit dataChanged(index, index); tv->reset(); tv->expandAll(); return true; } } } return false; } Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags f = FileModel::flags(index); if (index.column() == 1) f |= Qt::ItemIsUserCheckable; return f; } }; class FilterIdFileModel : public QSortFilterProxyModel { Q_OBJECT private: CheckableFileModel *internalModel; EntryClique *currentClique; public: FilterIdFileModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent), internalModel(nullptr), currentClique(nullptr) { /// nothing } void setCurrentClique(EntryClique *currentClique) { Q_ASSERT_X(internalModel != nullptr, "FilterIdFileModel::setCurrentClique(EntryClique *currentClique)", "internalModel is NULL"); internalModel->setCurrentClique(currentClique); this->currentClique = currentClique; invalidate(); } void setSourceModel(QAbstractItemModel *model) override { QSortFilterProxyModel::setSourceModel(model); internalModel = dynamic_cast<CheckableFileModel *>(model); Q_ASSERT_X(internalModel != nullptr, "FilterIdFileModel::setSourceModel(QAbstractItemModel *model)", "internalModel is NULL"); } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { Q_UNUSED(source_parent) if (internalModel != nullptr && currentClique != nullptr) { QSharedPointer<Entry> entry = internalModel->element(source_row).dynamicCast<Entry>(); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = currentClique->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) return true; // TODO does this work? } } return false; } }; class MergeWidget::MergeWidgetPrivate { private: MergeWidget *p; public: File *file; FileView *editor; QPushButton *buttonNext, *buttonPrev; QLabel *labelWhichClique; static const char *whichCliqueText; CheckableFileModel *model; FilterIdFileModel *filterModel; RadioButtonTreeView *alternativesView; AlternativesItemModel *alternativesItemModel; AlternativesItemDelegate *alternativesItemDelegate; int currentClique; QVector<EntryClique *> &cl; MergeWidgetPrivate(MergeWidget *parent, File *bibTeXFile, QVector<EntryClique *> &cliqueList) : p(parent), file(bibTeXFile), currentClique(0), cl(cliqueList) { setupGUI(); } void setupGUI() { p->setMinimumSize(p->fontMetrics().xHeight() * 96, p->fontMetrics().xHeight() * 64); p->setBaseSize(p->fontMetrics().xHeight() * 128, p->fontMetrics().xHeight() * 96); QBoxLayout *layout = new QVBoxLayout(p); QLabel *label = new QLabel(i18n("Select your duplicates"), p); layout->addWidget(label); QSplitter *splitter = new QSplitter(Qt::Vertical, p); layout->addWidget(splitter); editor = new FileView(QStringLiteral("MergeWidget"), splitter); editor->setItemDelegate(new FileDelegate(editor)); editor->setReadOnly(true); alternativesView = new RadioButtonTreeView(splitter); model = new CheckableFileModel(cl, alternativesView, p); model->setBibliographyFile(file); QBoxLayout *containerLayout = new QHBoxLayout(); layout->addLayout(containerLayout); containerLayout->addStretch(10); labelWhichClique = new QLabel(p); containerLayout->addWidget(labelWhichClique); buttonPrev = new QPushButton(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Previous Clique"), p); containerLayout->addWidget(buttonPrev, 1); buttonNext = new QPushButton(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Next Clique"), p); containerLayout->addWidget(buttonNext, 1); filterModel = new FilterIdFileModel(p); filterModel->setSourceModel(model); alternativesItemModel = new AlternativesItemModel(alternativesView); alternativesItemDelegate = new AlternativesItemDelegate(alternativesView); showCurrentClique(); connect(buttonPrev, &QPushButton::clicked, p, &MergeWidget::previousClique); connect(buttonNext, &QPushButton::clicked, p, &MergeWidget::nextClique); connect(editor, &FileView::doubleClicked, editor, &FileView::viewCurrentElement); } void showCurrentClique() { EntryClique *ec = cl[currentClique]; filterModel->setCurrentClique(ec); alternativesItemModel->setCurrentClique(ec); editor->setModel(filterModel); alternativesView->setModel(alternativesItemModel); alternativesView->setItemDelegate(alternativesItemDelegate); editor->reset(); alternativesView->reset(); alternativesView->expandAll(); buttonNext->setEnabled(currentClique >= 0 && currentClique < cl.count() - 1); buttonPrev->setEnabled(currentClique > 0); labelWhichClique->setText(i18n(whichCliqueText, currentClique + 1, cl.count())); } }; const char *MergeWidget::MergeWidgetPrivate::whichCliqueText = "Showing clique %1 of %2."; MergeWidget::MergeWidget(File *file, QVector<EntryClique *> &cliqueList, QWidget *parent) : QWidget(parent), d(new MergeWidgetPrivate(this, file, cliqueList)) { /// nothing } MergeWidget::~MergeWidget() { delete d; } void MergeWidget::previousClique() { if (d->currentClique > 0) { --d->currentClique; d->showCurrentClique(); } } void MergeWidget::nextClique() { if (d->currentClique >= 0 && d->currentClique < d->cl.count() - 1) { ++d->currentClique; d->showCurrentClique(); } } class FindDuplicatesUI::FindDuplicatesUIPrivate { public: KParts::Part *part; FileView *view; FindDuplicatesUIPrivate(FindDuplicatesUI *parent, KParts::Part *kpart, FileView *fileView) : part(kpart), view(fileView) { Q_UNUSED(parent) } }; FindDuplicatesUI::FindDuplicatesUI(KParts::Part *part, FileView *fileView) : QObject(), d(new FindDuplicatesUIPrivate(this, part, fileView)) { QAction *newAction = new QAction(QIcon::fromTheme(QStringLiteral("tab-duplicate")), i18n("Find Duplicates"), this); part->actionCollection()->addAction(QStringLiteral("findduplicates"), newAction); connect(newAction, &QAction::triggered, this, &FindDuplicatesUI::startDuplicatesSearch); } FindDuplicatesUI::~FindDuplicatesUI() { delete d; } void FindDuplicatesUI::startDuplicatesSearch() { // FIXME move to settings //bool ok = false; //int sensitivity = KInputDialog::getInteger(i18n("Sensitivity"), i18n("Enter a value for sensitivity.\n\nLow values (close to 0) require very similar entries for duplicate detection, larger values (close to 10000) are more likely to count entries as duplicates.\n\nPlease provide feedback to the developers if you have a suggestion for a better default value than 4000."), 4000, 0, 10000, 10, &ok, d->part->widget()); //if (!ok) sensitivity = 4000; int sensitivity = 4000; FileModel *model = d->view->fileModel(); if (model == nullptr) return; /// Full file, used to remove merged elements from /// Stays the same even when merging is restricted to selected elements File *originalFile = model->bibliographyFile(); /// File to be used to find duplicate in, /// may be only a subset of the original one if selection is used File *workingSetFile = originalFile; /// If more than one element but not all are selected in the main list view, /// ask the user if duplicates are to be searched only within the selection const int selectedRowsCount = d->view->selectedElements().count(); if (selectedRowsCount > 1 && selectedRowsCount < d->view->sortFilterProxyModel()->sourceModel()->rowCount() && KMessageBox::questionYesNo(d->part->widget(), i18n("Multiple elements are selected. Do you want to search for duplicates only within the selection or in the whole document?"), i18n("Search only in selection?"), KGuiItem(i18n("Only in selection")), KGuiItem(i18n("Whole document"))) == KMessageBox::Yes) { /// Yes, do only search for duplicates within selection, so copy all /// selected elements to a temporary new File object workingSetFile = new File(); const QModelIndexList mil = d->view->selectionModel()->selectedRows(); for (const QModelIndex &index : mil) workingSetFile->append(model->element(d->view->sortFilterProxyModel()->mapToSource(index).row())); } /// Actual duplicate finder, can be given a widget that will be the /// parent of a progress bar window and sensitivity value when to /// recognize two entries as being duplicates of each other FindDuplicates fd(d->part->widget(), sensitivity); QVector<EntryClique *> cliques; bool gotCanceled = fd.findDuplicateEntries(workingSetFile, cliques); if (gotCanceled) { /// Duplicate search was cancelled, e.g. by pressing the Cancel /// button on the progress bar window if (workingSetFile != originalFile) delete workingSetFile; return; } if (cliques.isEmpty()) KMessageBox::information(d->part->widget(), i18n("No duplicates were found."), i18n("No duplicates found")); else { /// Duplicates have been found, so let user choose how to handle duplicate fields /// Why is a QPointer used here you may wonder? Check here in case the link still works: /// https://blogs.kde.org/2009/03/26/how-crash-almost-every-qtkde-application-and-how-fix-it-0 QPointer<QDialog> dlg = new QDialog(d->part->widget()); dlg->setWindowTitle(i18n("Merge Duplicates")); MergeWidget *mw = new MergeWidget(workingSetFile, cliques, dlg); mw->layout()->setMargin(0); QBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(mw); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); if (dlg->exec() == QDialog::Accepted && MergeDuplicates::mergeDuplicateEntries(cliques, d->view->fileModel())) { /// If the user made selections on what to merge how /// AND the requested changes could be applied on the /// 'original' file (not the working set file), then /// notify the world that things have changed d->view->externalModification(); } /// Clean memory while (!cliques.isEmpty()) { EntryClique *ec = cliques.first(); cliques.removeFirst(); delete ec; } delete dlg; } if (workingSetFile != originalFile) delete workingSetFile; } #include "findduplicatesui.moc" diff --git a/src/gui/file/partwidget.cpp b/src/gui/file/partwidget.cpp index efeb04d0..7dac65b7 100644 --- a/src/gui/file/partwidget.cpp +++ b/src/gui/file/partwidget.cpp @@ -1,80 +1,80 @@ /*************************************************************************** - * Copyright (C) 2004-2014 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "partwidget.h" #include <QLayout> #include "widgets/filterbar.h" #include "fileview.h" #include "filedelegate.h" class PartWidget::Private { private: PartWidget *p; public: FileView *fileView; FilterBar *filterBar; Private(PartWidget *parent) : p(parent) { QBoxLayout *layout = new QVBoxLayout(parent); layout->setMargin(0); filterBar = new FilterBar(parent); layout->addWidget(filterBar, 0); fileView = new FileView(QStringLiteral("Main"), parent); layout->addWidget(fileView, 0xffffff); fileView->setFilterBar(filterBar); fileView->setItemDelegate(new FileDelegate(fileView)); fileView->setFocus(); connect(fileView, &FileView::searchFor, p, &PartWidget::searchFor); } }; PartWidget::PartWidget(QWidget *parent) : QWidget(parent), d(new PartWidget::Private(this)) { /// nothing } PartWidget::~PartWidget() { delete d; } FileView *PartWidget::fileView() { return d->fileView; } FilterBar *PartWidget::filterBar() { return d->filterBar; } void PartWidget::searchFor(const QString &text) { SortFilterFileModel::FilterQuery fq; - fq.combination = SortFilterFileModel::EveryTerm; + fq.combination = SortFilterFileModel::FilterCombination::EveryTerm; fq.field = QString(); fq.searchPDFfiles = false; fq.terms = QStringList() << text; d->filterBar->setFilter(fq); d->filterBar->setFocus(); } diff --git a/src/gui/file/sortfilterfilemodel.cpp b/src/gui/file/sortfilterfilemodel.cpp index 8da9f0a4..cbc385b6 100644 --- a/src/gui/file/sortfilterfilemodel.cpp +++ b/src/gui/file/sortfilterfilemodel.cpp @@ -1,263 +1,263 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "sortfilterfilemodel.h" #include <QRegularExpression> #include <BibTeXFields> #include <BibTeXEntries> #include <Entry> #include <Macro> #include <Preamble> #include <Comment> #include <FileInfo> SortFilterFileModel::SortFilterFileModel(QObject *parent) : QSortFilterProxyModel(parent), m_internalModel(nullptr) { - m_filterQuery.combination = AnyTerm; + m_filterQuery.combination = FilterCombination::AnyTerm; setSortRole(FileModel::SortRole); } void SortFilterFileModel::setSourceModel(QAbstractItemModel *model) { QSortFilterProxyModel::setSourceModel(model); m_internalModel = dynamic_cast<FileModel *>(model); } FileModel *SortFilterFileModel::fileSourceModel() const { return m_internalModel; } void SortFilterFileModel::updateFilter(const SortFilterFileModel::FilterQuery &filterQuery) { m_filterQuery = filterQuery; m_filterQuery.field = filterQuery.field.toLower(); /// required for comparison in filter code invalidate(); } bool SortFilterFileModel::simpleLessThan(const QModelIndex &left, const QModelIndex &right) const { const QString leftString = left.data(Qt::DisplayRole).toString().toLower(); const QString rightString = right.data(Qt::DisplayRole).toString().toLower(); const int cmp = QString::localeAwareCompare(leftString, rightString); if (cmp == 0) return left.row() < right.row(); else return cmp < 0; } bool SortFilterFileModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { int column = left.column(); Q_ASSERT_X(left.column() == right.column(), "bool SortFilterFileModel::lessThan(const QModelIndex &left, const QModelIndex &right) const", "Not comparing items in same column"); ///< assume that we only sort by column const FieldDescription &fd = BibTeXFields::instance().at(column); if (column == right.column() && (fd.upperCamelCase == QStringLiteral("Author") || fd.upperCamelCase == QStringLiteral("Editor"))) { /// special sorting for authors or editors: check all names, /// compare last and then first names /// first, check if two entries (and not e.g. comments) are to be compared QSharedPointer<Entry> entryA = m_internalModel->element(left.row()).dynamicCast<Entry>(); QSharedPointer<Entry> entryB = m_internalModel->element(right.row()).dynamicCast<Entry>(); if (entryA.isNull() || entryB.isNull()) return simpleLessThan(left, right); /// retrieve values of both cells Value valueA = entryA->value(fd.upperCamelCase); Value valueB = entryB->value(fd.upperCamelCase); if (valueA.isEmpty()) valueA = entryA->value(fd.upperCamelCaseAlt); if (valueB.isEmpty()) valueB = entryB->value(fd.upperCamelCaseAlt); /// if either value is empty, use default implementation if (valueA.isEmpty() || valueB.isEmpty()) return simpleLessThan(left, right); /// compare each person in both values for (Value::Iterator itA = valueA.begin(), itB = valueB.begin(); itA != valueA.end() && itB != valueB.end(); ++itA, ++itB) { static const QRegularExpression curlyRegExp(QStringLiteral("[{}]+")); QSharedPointer<Person> personA = (*itA).dynamicCast<Person>(); QSharedPointer<Person> personB = (*itB).dynamicCast<Person>(); /// not a Person object in value? fall back to default implementation if (personA.isNull() || personB.isNull()) return QSortFilterProxyModel::lessThan(left, right); /// get both values' next persons' last names for comparison QString nameA = personA->lastName().remove(curlyRegExp).toLower(); QString nameB = personB->lastName().remove(curlyRegExp).toLower(); int cmp = QString::localeAwareCompare(nameA, nameB); if (cmp < 0) return true; if (cmp > 0) return false; /// if last names were inconclusive ... /// get both values' next persons' first names for comparison nameA = personA->firstName().remove(curlyRegExp).toLower(); nameB = personB->firstName().remove(curlyRegExp).toLower(); cmp = QString::localeAwareCompare(nameA, nameB); if (cmp < 0) return true; if (cmp > 0) return false; // TODO Check for suffix and prefix? } /// comparison by names did not work (was not conclusive) /// fall back to default implementation return simpleLessThan(left, right); } else { /// if comparing two numbers, do not perform lexicographical sorting (i.e. 13 < 2), /// but numerical sorting instead (i.e. 13 > 2) const QString textLeft = left.data(Qt::DisplayRole).toString(); const QString textRight = right.data(Qt::DisplayRole).toString(); bool okLeft = false, okRight = false; int numberLeft = textLeft.toInt(&okLeft); int numberRight = textRight.toInt(&okRight); if (okLeft && okRight) return numberLeft < numberRight; /// everything else can be sorted by default implementation /// (i.e. alphabetically or lexicographically) return simpleLessThan(left, right); } } bool SortFilterFileModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent) const QSharedPointer<Element> rowElement = m_internalModel->element(source_row); Q_ASSERT_X(!rowElement.isNull(), "bool SortFilterFileModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const", "rowElement is NULL"); if (m_filterQuery.terms.isEmpty()) return true; /// empty filter query QScopedArrayPointer<bool> eachTerm(new bool[m_filterQuery.terms.count()]); for (int i = m_filterQuery.terms.count() - 1; i >= 0; --i) eachTerm[i] = false; const QSharedPointer<Entry> entry = rowElement.dynamicCast<Entry>(); if (!entry.isNull()) { /// if current row contains an Entry ... if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^id")) { /// Check entry's id const QString id = entry->id(); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : id.contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { /// Check entry's type const QString type = entry->type(); /// Check type's description ("Journal Article") const QString label = BibTeXEntries::instance().label(type); // TODO test for internationalized variants like "Artikel" or "bok" as well? int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : type.contains(*itsl, Qt::CaseInsensitive) || label.contains(*itsl, Qt::CaseInsensitive); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) if (m_filterQuery.field.isEmpty() || m_filterQuery.field == it.key().toLower()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : it.value().containsPattern(*itsl); } /// Test associated PDF files if (m_filterQuery.searchPDFfiles && m_filterQuery.field.isEmpty()) {///< not filtering for any specific field - const auto entryUrlList = FileInfo::entryUrls(entry, fileSourceModel()->bibliographyFile()->property(File::Url, QUrl()).toUrl(), FileInfo::TestExistenceYes); + const auto entryUrlList = FileInfo::entryUrls(entry, fileSourceModel()->bibliographyFile()->property(File::Url, QUrl()).toUrl(), FileInfo::TestExistence::Yes); for (const QUrl &url : entryUrlList) { if (url.isLocalFile() && url.fileName().endsWith(QStringLiteral(".pdf"))) { const QString text = FileInfo::pdfToText(url.toLocalFile()); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : text.contains(*itsl, Qt::CaseInsensitive); } } } int i = 0; if (m_filterQuery.field.isEmpty()) for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : entry->id().contains(*itsl, Qt::CaseInsensitive); } else { QSharedPointer<Macro> macro = rowElement.dynamicCast<Macro>(); if (!macro.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= macro->value().containsPattern(*itsl, Qt::CaseInsensitive) || macro->key().contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("macro"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } else { QSharedPointer<Comment> comment = rowElement.dynamicCast<Comment>(); if (!comment.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : comment->text().contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("comment"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } else { QSharedPointer<Preamble> preamble = rowElement.dynamicCast<Preamble>(); if (!preamble.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= preamble->value().containsPattern(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("preamble"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } } } } bool every = true, any = false; for (int i = m_filterQuery.terms.count() - 1; i >= 0; --i) { every &= eachTerm[i]; any |= eachTerm[i]; } - if (m_filterQuery.combination == SortFilterFileModel::AnyTerm) + if (m_filterQuery.combination == SortFilterFileModel::FilterCombination::AnyTerm) return any; else return every; } diff --git a/src/gui/file/sortfilterfilemodel.h b/src/gui/file/sortfilterfilemodel.h index 8f263362..4f9602dc 100644 --- a/src/gui/file/sortfilterfilemodel.h +++ b/src/gui/file/sortfilterfilemodel.h @@ -1,62 +1,62 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_SORTFILTERFILEMODEL_H #define KBIBTEX_GUI_SORTFILTERFILEMODEL_H #include <QSortFilterProxyModel> #include <models/FileModel> #include "kbibtexgui_export.h" /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXGUI_EXPORT SortFilterFileModel : public QSortFilterProxyModel { Q_OBJECT public: - enum FilterCombination {AnyTerm = 0, EveryTerm = 1 }; + enum class FilterCombination {AnyTerm, EveryTerm }; struct FilterQuery { QStringList terms; FilterCombination combination; QString field; bool searchPDFfiles; }; explicit SortFilterFileModel(QObject *parent = nullptr); void setSourceModel(QAbstractItemModel *model) override; FileModel *fileSourceModel() const; public slots: void updateFilter(const SortFilterFileModel::FilterQuery &); protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; private: FileModel *m_internalModel; SortFilterFileModel::FilterQuery m_filterQuery; bool simpleLessThan(const QModelIndex &left, const QModelIndex &right) const; }; #endif // KBIBTEX_GUI_SORTFILTERFILEMODEL_H diff --git a/src/gui/preferences/settingsfileexporterwidget.cpp b/src/gui/preferences/settingsfileexporterwidget.cpp index 9f452fc2..c8a64e79 100644 --- a/src/gui/preferences/settingsfileexporterwidget.cpp +++ b/src/gui/preferences/settingsfileexporterwidget.cpp @@ -1,216 +1,216 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "settingsfileexporterwidget.h" #include <QFormLayout> #include <QCheckBox> #include <QSpinBox> #include <QLineEdit> #include <QComboBox> #include <QPushButton> #include <qplatformdefs.h> #include <KLocalizedString> #include <KUrlRequester> #include <KLineEdit> /// required as KUrlRequester returns it #include <Preferences> #include <FileExporter> #include <LyX> #include "guihelper.h" #include "italictextitemmodel.h" #include "file/clipboard.h" class SettingsFileExporterWidget::SettingsFileExporterWidgetPrivate { private: SettingsFileExporterWidget *p; QComboBox *comboBoxCopyReferenceCmd; static const QString citeCmdToLabel; public: #ifdef QT_LSTAT QCheckBox *checkboxUseAutomaticLyXPipeDetection; #endif // QT_LSTAT QComboBox *comboBoxBackupScope; QSpinBox *spinboxNumberOfBackups; KUrlRequester *lineeditLyXPipePath; #ifdef QT_LSTAT QString lastUserInputLyXPipePath; #endif // QT_LSTAT SettingsFileExporterWidgetPrivate(SettingsFileExporterWidget *parent) : p(parent) { setupGUI(); } void loadState() { int row = GUIHelper::selectValue(comboBoxCopyReferenceCmd->model(), Preferences::instance().copyReferenceCommand(), ItalicTextItemModel::IdentifierRole); comboBoxCopyReferenceCmd->setCurrentIndex(row); const int index = qMax(0, comboBoxBackupScope->findData(static_cast<int>(Preferences::instance().backupScope()))); comboBoxBackupScope->setCurrentIndex(index); spinboxNumberOfBackups->setValue(qMax(0, qMin(spinboxNumberOfBackups->maximum(), Preferences::instance().numberOfBackups()))); #ifndef QT_LSTAT lineeditLyXPipePath->setText(Preferences::instance().lyXPipePath()); #else // QT_LSTAT checkboxUseAutomaticLyXPipeDetection->setChecked(Preferences::instance().lyXUseAutomaticPipeDetection()); lastUserInputLyXPipePath = Preferences::instance().lyXPipePath(); lineeditLyXPipePath->setText(lastUserInputLyXPipePath); p->automaticLyXDetectionToggled(checkboxUseAutomaticLyXPipeDetection->isChecked()); #endif // QT_LSTAT } bool saveState() { bool settingsGotChanged = false; settingsGotChanged |= Preferences::instance().setCopyReferenceCommand(comboBoxCopyReferenceCmd->itemData(comboBoxCopyReferenceCmd->currentIndex(), ItalicTextItemModel::IdentifierRole).toString()); settingsGotChanged |= Preferences::instance().setBackupScope(static_cast<Preferences::BackupScope>(comboBoxBackupScope->itemData(comboBoxBackupScope->currentIndex()).toInt())); settingsGotChanged |= Preferences::instance().setNumberOfBackups(spinboxNumberOfBackups->value()); #ifndef QT_LSTAT settingsGotChanged |= Preferences::instance().setLyXPipePath(lineeditLyXPipePath->text()); #else // QT_LSTAT settingsGotChanged |= Preferences::instance().setLyXUseAutomaticPipeDetection(checkboxUseAutomaticLyXPipeDetection->isChecked()); settingsGotChanged |= Preferences::instance().setLyXPipePath(checkboxUseAutomaticLyXPipeDetection->isChecked() ? lastUserInputLyXPipePath : lineeditLyXPipePath->text()); #endif // QT_LSTAT return settingsGotChanged; } void resetToDefaults() { int row = GUIHelper::selectValue(comboBoxCopyReferenceCmd->model(), QString(), Qt::UserRole); comboBoxCopyReferenceCmd->setCurrentIndex(row); - const int index = qMax(0, comboBoxBackupScope->findData(Preferences::defaultBackupScope)); + const int index = qMax(0, comboBoxBackupScope->findData(static_cast<int>(Preferences::defaultBackupScope))); comboBoxBackupScope->setCurrentIndex(index); spinboxNumberOfBackups->setValue(qMax(0, qMin(spinboxNumberOfBackups->maximum(), Preferences::defaultNumberOfBackups))); #ifndef QT_LSTAT const QString pipe = Preferences::defaultLyXPipePath; #else // QT_LSTAT checkboxUseAutomaticLyXPipeDetection->setChecked(Preferences::defaultLyXUseAutomaticPipeDetection); QString pipe = LyX::guessLyXPipeLocation(); if (pipe.isEmpty()) pipe = Preferences::defaultLyXPipePath; #endif // QT_LSTAT lineeditLyXPipePath->setText(pipe); } void setupGUI() { QFormLayout *layout = new QFormLayout(p); comboBoxCopyReferenceCmd = new QComboBox(p); comboBoxCopyReferenceCmd->setObjectName(QStringLiteral("comboBoxCopyReferenceCmd")); layout->addRow(i18n("Command for 'Copy Reference':"), comboBoxCopyReferenceCmd); ItalicTextItemModel *itim = new ItalicTextItemModel(); itim->addItem(i18n("No command"), QString()); for (const QString &citeCommand : Preferences::availableCopyReferenceCommands) itim->addItem(citeCmdToLabel.arg(citeCommand), citeCommand); comboBoxCopyReferenceCmd->setModel(itim); connect(comboBoxCopyReferenceCmd, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::changed); #ifdef QT_LSTAT checkboxUseAutomaticLyXPipeDetection = new QCheckBox(QString(), p); layout->addRow(i18n("Detect LyX pipe automatically:"), checkboxUseAutomaticLyXPipeDetection); connect(checkboxUseAutomaticLyXPipeDetection, &QCheckBox::toggled, p, &SettingsFileExporterWidget::changed); connect(checkboxUseAutomaticLyXPipeDetection, &QCheckBox::toggled, p, &SettingsFileExporterWidget::automaticLyXDetectionToggled); #endif // QT_LSTAT lineeditLyXPipePath = new KUrlRequester(p); layout->addRow(i18n("Manually specified LyX pipe:"), lineeditLyXPipePath); connect(qobject_cast<QLineEdit *>(lineeditLyXPipePath->lineEdit()), &QLineEdit::textEdited, p, &SettingsFileExporterWidget::changed); #if QT_VERSION >= 0x050b00 lineeditLyXPipePath->setMinimumWidth(lineeditLyXPipePath->fontMetrics().horizontalAdvance(QChar('W')) * 20); #else // QT_VERSION >= 0x050b00 lineeditLyXPipePath->setMinimumWidth(lineeditLyXPipePath->fontMetrics().width(QChar('W')) * 20); #endif // QT_VERSION >= 0x050b00 lineeditLyXPipePath->setFilter(QStringLiteral("inode/fifo")); lineeditLyXPipePath->setMode(KFile::ExistingOnly | KFile::LocalOnly); comboBoxBackupScope = new QComboBox(p); for (const auto &pair : Preferences::availableBackupScopes) comboBoxBackupScope->addItem(pair.second, static_cast<int>(pair.first)); layout->addRow(i18n("Backups when saving:"), comboBoxBackupScope); connect(comboBoxBackupScope, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::changed); spinboxNumberOfBackups = new QSpinBox(p); spinboxNumberOfBackups->setMinimum(1); spinboxNumberOfBackups->setMaximum(16); layout->addRow(i18n("Number of Backups:"), spinboxNumberOfBackups); connect(spinboxNumberOfBackups, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), p, &SettingsFileExporterWidget::changed); connect(comboBoxBackupScope, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &SettingsFileExporterWidget::updateGUI); } }; const QString SettingsFileExporterWidget::SettingsFileExporterWidgetPrivate::citeCmdToLabel = QStringLiteral("\\%1{") + QChar(0x2026) + QChar('}'); SettingsFileExporterWidget::SettingsFileExporterWidget(QWidget *parent) : SettingsAbstractWidget(parent), d(new SettingsFileExporterWidgetPrivate(this)) { d->loadState(); } SettingsFileExporterWidget::~SettingsFileExporterWidget() { delete d; } QString SettingsFileExporterWidget::label() const { return i18n("Saving and Exporting"); } QIcon SettingsFileExporterWidget::icon() const { return QIcon::fromTheme(QStringLiteral("document-save")); } void SettingsFileExporterWidget::loadState() { d->loadState(); } bool SettingsFileExporterWidget::saveState() { return d->saveState(); } void SettingsFileExporterWidget::resetToDefaults() { d->resetToDefaults(); } #ifdef QT_LSTAT void SettingsFileExporterWidget::automaticLyXDetectionToggled(bool isChecked) { d->lineeditLyXPipePath->setEnabled(!isChecked); if (isChecked) { d->lastUserInputLyXPipePath = d->lineeditLyXPipePath->text(); d->lineeditLyXPipePath->setText(LyX::guessLyXPipeLocation()); } else d->lineeditLyXPipePath->setText(d->lastUserInputLyXPipePath); } #endif // QT_LSTAT void SettingsFileExporterWidget::updateGUI() { - d->spinboxNumberOfBackups->setEnabled(d->comboBoxBackupScope->itemData(d->comboBoxBackupScope->currentIndex()).toInt() != static_cast<int>(Preferences::NoBackup)); + d->spinboxNumberOfBackups->setEnabled(d->comboBoxBackupScope->itemData(d->comboBoxBackupScope->currentIndex()).toInt() != static_cast<int>(Preferences::BackupScope::None)); } diff --git a/src/gui/preferences/settingsidsuggestionseditor.cpp b/src/gui/preferences/settingsidsuggestionseditor.cpp index df020ae7..a515f573 100644 --- a/src/gui/preferences/settingsidsuggestionseditor.cpp +++ b/src/gui/preferences/settingsidsuggestionseditor.cpp @@ -1,961 +1,961 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "settingsidsuggestionseditor.h" #include <limits> #include <QTimer> #include <QGridLayout> #include <QFormLayout> #include <QScrollArea> #include <QLabel> #include <QLineEdit> #include <QComboBox> #include <QSpinBox> #include <QCheckBox> #include <QMenu> #include <QPointer> #include <QPushButton> #include <QAction> #include <QDialogButtonBox> #include <KLocalizedString> #include <KIconLoader> #include "widgets/rangewidget.h" /** * @author Thomas Fischer */ class TokenWidget : public QGroupBox { Q_OBJECT protected: QGridLayout *gridLayout; QFormLayout *formLayout; public: explicit TokenWidget(QWidget *parent) : QGroupBox(parent) { gridLayout = new QGridLayout(this); formLayout = new QFormLayout(); gridLayout->addLayout(formLayout, 0, 0, 4, 1); gridLayout->setColumnStretch(0, 100); } void addButtons(QPushButton *buttonUp, QPushButton *buttonDown, QPushButton *buttonRemove) { gridLayout->setColumnMinimumWidth(1, 32); gridLayout->setColumnStretch(1, 1); gridLayout->setColumnStretch(2, 1); gridLayout->addWidget(buttonUp, 0, 2, 1, 1); buttonUp->setParent(this); gridLayout->addWidget(buttonDown, 1, 2, 1, 1); buttonDown->setParent(this); gridLayout->addWidget(buttonRemove, 2, 2, 1, 1); buttonRemove->setParent(this); } virtual QString toString() const = 0; }; /** * @author Thomas Fischer */ class AuthorWidget : public TokenWidget { Q_OBJECT private: RangeWidget *rangeWidgetAuthor; QCheckBox *checkBoxLastAuthor; QLabel *labelAuthorRange; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; private slots: void updateRangeLabel() { const int lower = rangeWidgetAuthor->lowerValue(); const int upper = rangeWidgetAuthor->upperValue(); const int max = rangeWidgetAuthor->maximum(); labelAuthorRange->setText(IdSuggestions::formatAuthorRange(lower, upper == max ? std::numeric_limits<int>::max() : upper, checkBoxLastAuthor->isChecked())); } public: AuthorWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Authors")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); formLayout->addRow(i18n("Author Range:"), boxLayout); static const QStringList authorRange {i18n("First author"), i18n("Second author"), i18n("Third author"), i18n("Fourth author"), i18n("Fifth author"), i18n("Sixth author"), i18n("Seventh author"), i18n("Eighth author"), i18n("Ninth author"), i18n("Tenth author"), i18n("|Last author")}; rangeWidgetAuthor = new RangeWidget(authorRange, this); boxLayout->addWidget(rangeWidgetAuthor); rangeWidgetAuthor->setLowerValue(info.startWord); rangeWidgetAuthor->setUpperValue(qMin(authorRange.size() - 1, info.endWord)); checkBoxLastAuthor = new QCheckBox(i18n("... and last author"), this); boxLayout->addWidget(checkBoxLastAuthor); labelAuthorRange = new QLabel(this); boxLayout->addWidget(labelAuthorRange); #if QT_VERSION >= 0x050b00 const int maxWidth = qMax(labelAuthorRange->fontMetrics().horizontalAdvance(i18n("From first author to author %1 and last author", 88)), labelAuthorRange->fontMetrics().horizontalAdvance(i18n("From author %1 to author %2 and last author", 88, 88))); #else // QT_VERSION >= 0x050b00 const int maxWidth = qMax(labelAuthorRange->fontMetrics().width(i18n("From first author to author %1 and last author", 88)), labelAuthorRange->fontMetrics().width(i18n("From author %1 to author %2 and last author", 88, 88))); #endif // QT_VERSION >= 0x050b00 labelAuthorRange->setMinimumWidth(maxWidth); comboBoxChangeCase = new QComboBox(this); - comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); - comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); - comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); - comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); + comboBoxChangeCase->addItem(i18n("No change"), static_cast<int>(IdSuggestions::CaseChange::None)); + comboBoxChangeCase->addItem(i18n("To upper case"), static_cast<int>(IdSuggestions::CaseChange::ToUpper)); + comboBoxChangeCase->addItem(i18n("To lower case"), static_cast<int>(IdSuggestions::CaseChange::ToLower)); + comboBoxChangeCase->addItem(i18n("To CamelCase"), static_cast<int>(IdSuggestions::CaseChange::ToCamelCase)); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast<int>(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, this, &AuthorWidget::updateRangeLabel); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, this, &AuthorWidget::updateRangeLabel); connect(checkBoxLastAuthor, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(checkBoxLastAuthor, &QCheckBox::toggled, this, &AuthorWidget::updateRangeLabel); connect(comboBoxChangeCase, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); updateRangeLabel(); } QString toString() const override { QString result = QStringLiteral("A"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast<IdSuggestions::CaseChange>(comboBoxChangeCase->currentIndex()); - if (caseChange == IdSuggestions::ccToLower) + if (caseChange == IdSuggestions::CaseChange::ToLower) result.append(QStringLiteral("l")); - else if (caseChange == IdSuggestions::ccToUpper) + else if (caseChange == IdSuggestions::CaseChange::ToUpper) result.append(QStringLiteral("u")); - else if (caseChange == IdSuggestions::ccToCamelCase) + else if (caseChange == IdSuggestions::CaseChange::ToCamelCase) result.append(QStringLiteral("c")); if (rangeWidgetAuthor->lowerValue() > 0 || rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum()) result.append(QString(QStringLiteral("w%1%2")).arg(rangeWidgetAuthor->lowerValue()).arg(rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum() ? QString::number(rangeWidgetAuthor->upperValue()) : QStringLiteral("I"))); if (checkBoxLastAuthor->isChecked()) result.append(QStringLiteral("L")); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Thomas Fischer */ class YearWidget : public TokenWidget { Q_OBJECT private: QComboBox *comboBoxDigits; public: YearWidget(int digits, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Year")); comboBoxDigits = new QComboBox(this); comboBoxDigits->addItem(i18n("2 digits"), 2); comboBoxDigits->addItem(i18n("4 digits"), 4); formLayout->addRow(i18n("Digits:"), comboBoxDigits); comboBoxDigits->setCurrentIndex(comboBoxDigits->findData(digits)); connect(comboBoxDigits, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { const int year = comboBoxDigits->itemData(comboBoxDigits->currentIndex()).toInt(); QString result = year == 4 ? QStringLiteral("Y") : QStringLiteral("y"); return result; } }; /** * @author Thomas Fischer */ class VolumeWidget : public TokenWidget { Q_OBJECT private: QLabel *labelCheckmark; public: VolumeWidget(IdSuggestionsEditWidget *, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Volume")); labelCheckmark = new QLabel(this); labelCheckmark->setPixmap(KIconLoader::global()->loadMimeTypeIcon(QStringLiteral("dialog-ok-apply"), KIconLoader::Small)); formLayout->addRow(i18n("Volume:"), labelCheckmark); } QString toString() const override { return QStringLiteral("v"); } }; /** * @author Thomas Fischer */ class PageNumberWidget : public TokenWidget { Q_OBJECT private: QLabel *labelCheckmark; public: PageNumberWidget(IdSuggestionsEditWidget *, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Page Number")); labelCheckmark = new QLabel(this); labelCheckmark->setPixmap(KIconLoader::global()->loadMimeTypeIcon(QStringLiteral("dialog-ok-apply"), KIconLoader::Small)); formLayout->addRow(i18n("First page's number:"), labelCheckmark); } QString toString() const override { return QStringLiteral("p"); } }; /** * @author Thomas Fischer */ class TitleWidget : public TokenWidget { Q_OBJECT private: RangeWidget *rangeWidgetAuthor; QLabel *labelWordsRange; QCheckBox *checkBoxRemoveSmallWords; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; private slots: void updateRangeLabel() { const int lower = rangeWidgetAuthor->lowerValue(); const int upper = rangeWidgetAuthor->upperValue(); const int max = rangeWidgetAuthor->maximum(); if (lower == 0 && upper == 0) labelWordsRange->setText(i18n("First word only")); else if (lower == 1 && upper >= max) labelWordsRange->setText(i18n("All but first word")); else if (lower == 0 && upper >= max) labelWordsRange->setText(i18n("From first to last word")); else if (lower > 0 && upper >= max) labelWordsRange->setText(i18n("From word %1 to last word", lower + 1)); else if (lower == 0 && upper < max) labelWordsRange->setText(i18n("From first word to word %1", upper + 1)); else labelWordsRange->setText(i18n("From word %1 to word %2", lower + 1, upper + 1)); } public: TitleWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, bool removeSmallWords, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Title")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); formLayout->addRow(i18n("Word Range:"), boxLayout); static const QStringList wordRange {i18n("First word"), i18n("Second word"), i18n("Third word"), i18n("Fourth word"), i18n("Fifth word"), i18n("Sixth word"), i18n("Seventh word"), i18n("Eighth word"), i18n("Ninth word"), i18n("Tenth word"), i18n("|Last word")}; rangeWidgetAuthor = new RangeWidget(wordRange, this); boxLayout->addWidget(rangeWidgetAuthor); rangeWidgetAuthor->setLowerValue(info.startWord); rangeWidgetAuthor->setUpperValue(qMin(rangeWidgetAuthor->maximum(), info.endWord)); labelWordsRange = new QLabel(this); boxLayout->addWidget(labelWordsRange); #if QT_VERSION >= 0x050b00 const int a = qMax(labelWordsRange->fontMetrics().horizontalAdvance(i18n("From first to last word")), labelWordsRange->fontMetrics().horizontalAdvance(i18n("From word %1 to last word", 88))); const int b = qMax(labelWordsRange->fontMetrics().horizontalAdvance(i18n("From first word to word %1", 88)), labelWordsRange->fontMetrics().horizontalAdvance(i18n("From word %1 to word %2", 88, 88))); #else // QT_VERSION >= 0x050b00 const int a = qMax(labelWordsRange->fontMetrics().width(i18n("From first to last word")), labelWordsRange->fontMetrics().width(i18n("From word %1 to last word", 88))); const int b = qMax(labelWordsRange->fontMetrics().width(i18n("From first word to word %1", 88)), labelWordsRange->fontMetrics().width(i18n("From word %1 to word %2", 88, 88))); #endif // QT_VERSION >= 0x050b00 labelWordsRange->setMinimumWidth(qMax(a, b)); checkBoxRemoveSmallWords = new QCheckBox(i18n("Remove"), this); formLayout->addRow(i18n("Small words:"), checkBoxRemoveSmallWords); checkBoxRemoveSmallWords->setChecked(removeSmallWords); comboBoxChangeCase = new QComboBox(this); - comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); - comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); - comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); - comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); + comboBoxChangeCase->addItem(i18n("No change"), static_cast<int>(IdSuggestions::CaseChange::None)); + comboBoxChangeCase->addItem(i18n("To upper case"), static_cast<int>(IdSuggestions::CaseChange::ToUpper)); + comboBoxChangeCase->addItem(i18n("To lower case"), static_cast<int>(IdSuggestions::CaseChange::ToLower)); + comboBoxChangeCase->addItem(i18n("To CamelCase"), static_cast<int>(IdSuggestions::CaseChange::ToCamelCase)); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast<int>(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, isew, &IdSuggestionsEditWidget::updatePreview); connect(rangeWidgetAuthor, &RangeWidget::lowerValueChanged, this, &TitleWidget::updateRangeLabel); connect(rangeWidgetAuthor, &RangeWidget::upperValueChanged, this, &TitleWidget::updateRangeLabel); connect(checkBoxRemoveSmallWords, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(comboBoxChangeCase, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); updateRangeLabel(); } QString toString() const override { QString result = checkBoxRemoveSmallWords->isChecked() ? QStringLiteral("T") : QStringLiteral("t"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast<IdSuggestions::CaseChange>(comboBoxChangeCase->currentIndex()); - if (caseChange == IdSuggestions::ccToLower) + if (caseChange == IdSuggestions::CaseChange::ToLower) result.append(QStringLiteral("l")); - else if (caseChange == IdSuggestions::ccToUpper) + else if (caseChange == IdSuggestions::CaseChange::ToUpper) result.append(QStringLiteral("u")); - else if (caseChange == IdSuggestions::ccToCamelCase) + else if (caseChange == IdSuggestions::CaseChange::ToCamelCase) result.append(QStringLiteral("c")); if (rangeWidgetAuthor->lowerValue() > 0 || rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum()) result.append(QString(QStringLiteral("w%1%2")).arg(rangeWidgetAuthor->lowerValue()).arg(rangeWidgetAuthor->upperValue() < rangeWidgetAuthor->maximum() ? QString::number(rangeWidgetAuthor->upperValue()) : QStringLiteral("I"))); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Thomas Fischer */ class JournalWidget : public TokenWidget { Q_OBJECT private: QCheckBox *checkBoxRemoveSmallWords; QComboBox *comboBoxChangeCase; QLineEdit *lineEditTextInBetween; QSpinBox *spinBoxLength; public: JournalWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, bool removeSmallWords, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Journal")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); checkBoxRemoveSmallWords = new QCheckBox(i18n("Remove"), this); formLayout->addRow(i18n("Small words:"), checkBoxRemoveSmallWords); checkBoxRemoveSmallWords->setChecked(removeSmallWords); comboBoxChangeCase = new QComboBox(this); - comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); - comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); - comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); - comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); + comboBoxChangeCase->addItem(i18n("No change"), static_cast<int>(IdSuggestions::CaseChange::None)); + comboBoxChangeCase->addItem(i18n("To upper case"), static_cast<int>(IdSuggestions::CaseChange::ToUpper)); + comboBoxChangeCase->addItem(i18n("To lower case"), static_cast<int>(IdSuggestions::CaseChange::ToLower)); + comboBoxChangeCase->addItem(i18n("To CamelCase"), static_cast<int>(IdSuggestions::CaseChange::ToCamelCase)); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast<int>(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices lineEditTextInBetween = new QLineEdit(this); formLayout->addRow(i18n("Text in between:"), lineEditTextInBetween); lineEditTextInBetween->setText(info.inBetween); spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(checkBoxRemoveSmallWords, &QCheckBox::toggled, isew, &IdSuggestionsEditWidget::updatePreview); connect(comboBoxChangeCase, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(lineEditTextInBetween, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = checkBoxRemoveSmallWords->isChecked() ? QStringLiteral("J") : QStringLiteral("j"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast<IdSuggestions::CaseChange>(comboBoxChangeCase->currentIndex()); - if (caseChange == IdSuggestions::ccToLower) + if (caseChange == IdSuggestions::CaseChange::ToLower) result.append(QStringLiteral("l")); - else if (caseChange == IdSuggestions::ccToUpper) + else if (caseChange == IdSuggestions::CaseChange::ToUpper) result.append(QStringLiteral("u")); - else if (caseChange == IdSuggestions::ccToCamelCase) + else if (caseChange == IdSuggestions::CaseChange::ToCamelCase) result.append(QStringLiteral("c")); const QString text = lineEditTextInBetween->text(); if (!text.isEmpty()) result.append(QStringLiteral("\"")).append(text); return result; } }; /** * @author Erik Quaeghebeur */ class TypeWidget : public TokenWidget { Q_OBJECT private: QComboBox *comboBoxChangeCase; QSpinBox *spinBoxLength; public: TypeWidget(const struct IdSuggestions::IdSuggestionTokenInfo &info, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Type")); QBoxLayout *boxLayout = new QVBoxLayout(); boxLayout->setMargin(0); comboBoxChangeCase = new QComboBox(this); - comboBoxChangeCase->addItem(i18n("No change"), IdSuggestions::ccNoChange); - comboBoxChangeCase->addItem(i18n("To upper case"), IdSuggestions::ccToUpper); - comboBoxChangeCase->addItem(i18n("To lower case"), IdSuggestions::ccToLower); - comboBoxChangeCase->addItem(i18n("To CamelCase"), IdSuggestions::ccToCamelCase); + comboBoxChangeCase->addItem(i18n("No change"), static_cast<int>(IdSuggestions::CaseChange::None)); + comboBoxChangeCase->addItem(i18n("To upper case"), static_cast<int>(IdSuggestions::CaseChange::ToUpper)); + comboBoxChangeCase->addItem(i18n("To lower case"), static_cast<int>(IdSuggestions::CaseChange::ToLower)); + comboBoxChangeCase->addItem(i18n("To CamelCase"), static_cast<int>(IdSuggestions::CaseChange::ToCamelCase)); formLayout->addRow(i18n("Change casing:"), comboBoxChangeCase); comboBoxChangeCase->setCurrentIndex(static_cast<int>(info.caseChange)); /// enum has numbers assigned to cases and combo box has same indices spinBoxLength = new QSpinBox(this); formLayout->addRow(i18n("Only first characters:"), spinBoxLength); spinBoxLength->setSpecialValueText(i18n("No limitation")); spinBoxLength->setMinimum(0); spinBoxLength->setMaximum(9); spinBoxLength->setValue(info.len == 0 || info.len > 9 ? 0 : info.len); connect(comboBoxChangeCase, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), isew, &IdSuggestionsEditWidget::updatePreview); connect(spinBoxLength, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged), isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = QStringLiteral("e"); if (spinBoxLength->value() > 0) result.append(QString::number(spinBoxLength->value())); const IdSuggestions::CaseChange caseChange = static_cast<IdSuggestions::CaseChange>(comboBoxChangeCase->currentIndex()); - if (caseChange == IdSuggestions::ccToLower) + if (caseChange == IdSuggestions::CaseChange::ToLower) result.append(QStringLiteral("l")); - else if (caseChange == IdSuggestions::ccToUpper) + else if (caseChange == IdSuggestions::CaseChange::ToUpper) result.append(QStringLiteral("u")); - else if (caseChange == IdSuggestions::ccToCamelCase) + else if (caseChange == IdSuggestions::CaseChange::ToCamelCase) result.append(QStringLiteral("c")); return result; } }; /** * @author Thomas Fischer */ class TextWidget : public TokenWidget { Q_OBJECT private: QLineEdit *lineEditText; public: TextWidget(const QString &text, IdSuggestionsEditWidget *isew, QWidget *parent) : TokenWidget(parent) { setTitle(i18n("Text")); lineEditText = new QLineEdit(this); formLayout->addRow(i18n("Text:"), lineEditText); lineEditText->setText(text); connect(lineEditText, &QLineEdit::textEdited, isew, &IdSuggestionsEditWidget::updatePreview); } QString toString() const override { QString result = QStringLiteral("\"") + lineEditText->text(); return result; } }; class IdSuggestionsEditWidget::IdSuggestionsEditWidgetPrivate { private: IdSuggestionsEditWidget *p; public: enum TokenType {ttTitle, ttAuthor, ttYear, ttJournal, ttType, ttText, ttVolume, ttPageNumber}; enum Location {locAtTop, locAtBottom}; QWidget *container; QBoxLayout *containerLayout; QList<TokenWidget *> widgetList; QLabel *labelPreview; QPushButton *buttonAddTokenAtTop, *buttonAddTokenAtBottom; const Entry *previewEntry; QScrollArea *area; IdSuggestionsEditWidgetPrivate(const Entry *pe, IdSuggestionsEditWidget *parent) : p(parent), previewEntry(pe) { setupGUI(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); labelPreview = new QLabel(p); layout->addWidget(labelPreview, 0, 0, 1, 1); layout->setColumnStretch(0, 100); area = new QScrollArea(p); layout->addWidget(area, 1, 0, 1, 1); area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); container = new QWidget(area); area->setWidget(container); area->setWidgetResizable(true); containerLayout = new QVBoxLayout(container); area->setMinimumSize(384, 256); buttonAddTokenAtTop = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add at top"), container); containerLayout->addWidget(buttonAddTokenAtTop, 0); containerLayout->addStretch(1); buttonAddTokenAtBottom = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add at bottom"), container); containerLayout->addWidget(buttonAddTokenAtBottom, 0); QMenu *menuAddToken = new QMenu(p); buttonAddTokenAtTop->setMenu(menuAddToken); QAction *action = menuAddToken->addAction(i18n("Title")); connect(action, &QAction::triggered, p, [this]() { addToken(ttTitle, locAtTop); }); action = menuAddToken->addAction(i18n("Author")); connect(action, &QAction::triggered, p, [this]() { addToken(ttAuthor, locAtTop); }); action = menuAddToken->addAction(i18n("Year")); connect(action, &QAction::triggered, p, [this]() { addToken(ttYear, locAtTop); }); action = menuAddToken->addAction(i18n("Journal")); connect(action, &QAction::triggered, p, [this]() { addToken(ttJournal, locAtTop); }); action = menuAddToken->addAction(i18n("Type")); connect(action, &QAction::triggered, p, [this]() { addToken(ttType, locAtTop); }); action = menuAddToken->addAction(i18n("Volume")); connect(action, &QAction::triggered, p, [this]() { addToken(ttVolume, locAtTop); }); action = menuAddToken->addAction(i18n("Page Number")); connect(action, &QAction::triggered, p, [this]() { addToken(ttPageNumber, locAtTop); }); action = menuAddToken->addAction(i18n("Text")); connect(action, &QAction::triggered, p, [this]() { addToken(ttText, locAtTop); }); menuAddToken = new QMenu(p); buttonAddTokenAtBottom->setMenu(menuAddToken); action = menuAddToken->addAction(i18n("Title")); connect(action, &QAction::triggered, p, [this]() { addToken(ttTitle, locAtBottom); }); action = menuAddToken->addAction(i18n("Author")); connect(action, &QAction::triggered, p, [this]() { addToken(ttAuthor, locAtBottom); }); action = menuAddToken->addAction(i18n("Year")); connect(action, &QAction::triggered, p, [this]() { addToken(ttYear, locAtBottom); }); action = menuAddToken->addAction(i18n("Journal")); connect(action, &QAction::triggered, p, [this]() { addToken(ttJournal, locAtBottom); }); action = menuAddToken->addAction(i18n("Type")); connect(action, &QAction::triggered, p, [this]() { addToken(ttType, locAtBottom); }); action = menuAddToken->addAction(i18n("Volume")); connect(action, &QAction::triggered, p, [this]() { addToken(ttVolume, locAtBottom); }); action = menuAddToken->addAction(i18n("Page Number")); connect(action, &QAction::triggered, p, [this]() { addToken(ttPageNumber, locAtBottom); }); action = menuAddToken->addAction(i18n("Text")); connect(action, &QAction::triggered, p, [this]() { addToken(ttText, locAtBottom); }); } void addManagementButtons(TokenWidget *tokenWidget) { if (tokenWidget != nullptr) { QPushButton *buttonUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), tokenWidget); QPushButton *buttonDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), tokenWidget); QPushButton *buttonRemove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), tokenWidget); tokenWidget->addButtons(buttonUp, buttonDown, buttonRemove); connect(buttonUp, &QPushButton::clicked, p, [this, tokenWidget]() { moveUpToken(tokenWidget); }); connect(buttonDown, &QPushButton::clicked, p, [this, tokenWidget]() { moveDownToken(tokenWidget); }); connect(buttonRemove, &QPushButton::clicked, p, [this, tokenWidget]() { removeToken(tokenWidget); }); } } void add(const TokenType tokenType, const Location location) { TokenWidget *tokenWidget = nullptr; switch (tokenType) { case ttTitle: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = std::numeric_limits<int>::max(); info.startWord = 0; info.endWord = std::numeric_limits<int>::max(); info.lastWord = false; - info.caseChange = IdSuggestions::ccNoChange; + info.caseChange = IdSuggestions::CaseChange::None; tokenWidget = new TitleWidget(info, true, p, container); } break; case ttAuthor: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = std::numeric_limits<int>::max(); info.startWord = 0; info.endWord = std::numeric_limits<int>::max(); info.lastWord = false; - info.caseChange = IdSuggestions::ccNoChange; + info.caseChange = IdSuggestions::CaseChange::None; tokenWidget = new AuthorWidget(info, p, container); } break; case ttYear: tokenWidget = new YearWidget(4, p, container); break; case ttJournal: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = 1; info.startWord = 0; info.endWord = std::numeric_limits<int>::max(); info.lastWord = false; - info.caseChange = IdSuggestions::ccNoChange; + info.caseChange = IdSuggestions::CaseChange::None; tokenWidget = new JournalWidget(info, true, p, container); } break; case ttType: { struct IdSuggestions::IdSuggestionTokenInfo info; info.inBetween = QString(); info.len = std::numeric_limits<int>::max(); info.startWord = 0; info.endWord = std::numeric_limits<int>::max(); info.lastWord = false; - info.caseChange = IdSuggestions::ccNoChange; + info.caseChange = IdSuggestions::CaseChange::None; tokenWidget = new TypeWidget(info, p, container); } break; case ttText: tokenWidget = new TextWidget(QString(), p, container); break; case ttVolume: tokenWidget = new VolumeWidget(p, container); break; case ttPageNumber: tokenWidget = new PageNumberWidget(p, container); break; } if (tokenWidget != nullptr) { const int pos = location == locAtTop ? 1 : containerLayout->count() - 2; if (location == locAtTop) widgetList.prepend(tokenWidget); else if (location == locAtBottom) widgetList.append(tokenWidget); containerLayout->insertWidget(pos, tokenWidget, 1); addManagementButtons(tokenWidget); } } void reset(const QString &formatString) { while (!widgetList.isEmpty()) delete widgetList.takeFirst(); const QStringList tokenList = formatString.split(QStringLiteral("|"), QString::SkipEmptyParts); for (const QString &token : tokenList) { TokenWidget *tokenWidget = nullptr; if (token[0] == 'a' || token[0] == 'A' || token[0] == 'z') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); /// Support deprecated 'a' and 'z' cases if (token[0] == 'a') info.startWord = info.endWord = 0; else if (token[0] == 'z') { info.startWord = 1; info.endWord = std::numeric_limits<int>::max(); } tokenWidget = new AuthorWidget(info, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'y') { tokenWidget = new YearWidget(2, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'Y') { tokenWidget = new YearWidget(4, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 't' || token[0] == 'T') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new TitleWidget(info, token[0].isUpper(), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'j' || token[0] == 'J') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new JournalWidget(info, token[0].isUpper(), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'e') { struct IdSuggestions::IdSuggestionTokenInfo info = p->evalToken(token.mid(1)); tokenWidget = new TypeWidget(info, p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'v') { tokenWidget = new VolumeWidget(p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == 'p') { tokenWidget = new PageNumberWidget(p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } else if (token[0] == '"') { tokenWidget = new TextWidget(token.mid(1), p, container); widgetList << tokenWidget; containerLayout->insertWidget(containerLayout->count() - 2, tokenWidget, 1); } if (tokenWidget != nullptr) addManagementButtons(tokenWidget); } p->updatePreview(); } QString apply() { QStringList result; result.reserve(widgetList.size()); for (TokenWidget *widget : const_cast<const QList<TokenWidget *> &>(widgetList)) result << widget->toString(); return result.join(QStringLiteral("|")); } void addToken(const IdSuggestionsEditWidgetPrivate::TokenType cmd, const IdSuggestionsEditWidgetPrivate::Location location) { if (location == IdSuggestionsEditWidgetPrivate::locAtTop) { add(cmd, IdSuggestionsEditWidgetPrivate::locAtTop); QTimer::singleShot(50, p, [this]() { area->ensureWidgetVisible(buttonAddTokenAtTop); }); } else if (location == IdSuggestionsEditWidgetPrivate::locAtBottom) { add(cmd, IdSuggestionsEditWidgetPrivate::locAtBottom); QTimer::singleShot(50, p, [this]() { area->ensureWidgetVisible(buttonAddTokenAtBottom); }); } p->updatePreview(); } void moveUpToken(TokenWidget *tokenWidget) { const int curPos = widgetList.indexOf(tokenWidget); if (curPos > 0) { widgetList.removeAt(curPos); const int layoutPos = containerLayout->indexOf(tokenWidget); containerLayout->removeWidget(tokenWidget); widgetList.insert(curPos - 1, tokenWidget); containerLayout->insertWidget(layoutPos - 1, tokenWidget, 1); p->updatePreview(); } } void moveDownToken(TokenWidget *tokenWidget) { const int curPos = widgetList.indexOf(tokenWidget); if (curPos < widgetList.size() - 1) { widgetList.removeAt(curPos); const int layoutPos = containerLayout->indexOf(tokenWidget); containerLayout->removeWidget(tokenWidget); widgetList.insert(curPos + 1, tokenWidget); containerLayout->insertWidget(layoutPos + 1, tokenWidget, 1); p->updatePreview(); } } void removeToken(TokenWidget *tokenWidget) { widgetList.removeOne(tokenWidget); containerLayout->removeWidget(tokenWidget); tokenWidget->deleteLater(); p->updatePreview(); } }; IdSuggestionsEditWidget::IdSuggestionsEditWidget(const Entry *previewEntry, QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), IdSuggestions(), d(new IdSuggestionsEditWidgetPrivate(previewEntry, this)) { /// nothing } IdSuggestionsEditWidget::~IdSuggestionsEditWidget() { // TODO } void IdSuggestionsEditWidget::setFormatString(const QString &formatString) { d->reset(formatString); } QString IdSuggestionsEditWidget::formatString() const { return d->apply(); } void IdSuggestionsEditWidget::updatePreview() { const QString formatString = d->apply(); d->labelPreview->setText(formatId(*d->previewEntry, formatString)); d->labelPreview->setToolTip(i18n("<qt>Structure:<ul><li>%1</li></ul>Example: %2</qt>", formatStrToHuman(formatString).join(QStringLiteral("</li><li>")), formatId(*d->previewEntry, formatString))); } IdSuggestionsEditDialog::IdSuggestionsEditDialog(QWidget *parent, Qt::WindowFlags flags) : QDialog(parent, flags) { setWindowTitle(i18n("Edit Id Suggestion")); } IdSuggestionsEditDialog::~IdSuggestionsEditDialog() { /// nothing } QString IdSuggestionsEditDialog::editSuggestion(const Entry *previewEntry, const QString &suggestion, QWidget *parent) { QPointer<IdSuggestionsEditDialog> dlg = new IdSuggestionsEditDialog(parent); QBoxLayout *boxLayout = new QVBoxLayout(dlg); IdSuggestionsEditWidget *widget = new IdSuggestionsEditWidget(previewEntry, dlg); boxLayout->addWidget(widget); QDialogButtonBox *dbb = new QDialogButtonBox(dlg); dbb->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); boxLayout->addWidget(dbb); connect(dbb->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(dbb->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); widget->setFormatString(suggestion); if (dlg->exec() == Accepted) { const QString formatString = widget->formatString(); delete dlg; return formatString; } delete dlg; /// Return unmodified original suggestion return suggestion; } #include "settingsidsuggestionseditor.moc" diff --git a/src/gui/valuelistmodel.cpp b/src/gui/valuelistmodel.cpp index e2dee578..bc703ae4 100644 --- a/src/gui/valuelistmodel.cpp +++ b/src/gui/valuelistmodel.cpp @@ -1,577 +1,577 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "valuelistmodel.h" #include <typeinfo> #include <QApplication> #include <QTextDocument> #include <QAbstractTextDocumentLayout> #include <QListView> #include <QLineEdit> #include <QGridLayout> #include <QStringListModel> #include <QPainter> #include <QFrame> #include <QLayout> #include <QHeaderView> #include <QComboBox> #include <KLocalizedString> #include <KColorScheme> #include <BibTeXFields> #include <Preferences> #include <Entry> #include <models/FileModel> #include "field/fieldlineedit.h" #include "logging_gui.h" QWidget *ValueListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &sovi, const QModelIndex &index) const { if (index.column() == 0) { const FieldDescription &fd = BibTeXFields::instance().find(m_fieldName); FieldLineEdit *fieldLineEdit = new FieldLineEdit(fd.preferredTypeFlag, fd.typeFlags, false, parent); fieldLineEdit->setAutoFillBackground(true); return fieldLineEdit; } else return QStyledItemDelegate::createEditor(parent, sovi, index); } void ValueListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == 0) { FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor); if (fieldLineEdit != nullptr) fieldLineEdit->reset(index.model()->data(index, Qt::EditRole).value<Value>()); } } void ValueListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor); if (fieldLineEdit != nullptr) { Value v; fieldLineEdit->apply(v); if (v.count() == 1) /// field should contain exactly one value item (no zero, not two or more) model->setData(index, QVariant::fromValue(v)); } } QSize ValueListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(qMax(size.height(), option.fontMetrics.height() * 3 / 2)); // TODO calculate height better return size; } void ValueListDelegate::commitAndCloseEditor() { QLineEdit *editor = qobject_cast<QLineEdit *>(sender()); emit commitData(editor); emit closeEditor(editor); } void ValueListDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const { QStyledItemDelegate::initStyleOption(option, index); if (option->decorationPosition != QStyleOptionViewItem::Top) { /// remove text from style (do not draw text) option->text.clear(); } } void ValueListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &_option, const QModelIndex &index) const { QStyleOptionViewItem option = _option; /// code heavily inspired by kdepimlibs-4.6.3/akonadi/collectionstatisticsdelegate.cpp /// save painter's state, restored before leaving this function painter->save(); /// first, paint the basic, but without the text. We remove the text /// in initStyleOption(), which gets called by QStyledItemDelegate::paint(). QStyledItemDelegate::paint(painter, option, index); /// now, we retrieve the correct style option by calling intiStyleOption from /// the superclass. QStyledItemDelegate::initStyleOption(&option, index); QString field = option.text; /// now calculate the rectangle for the text QStyle *s = m_parent->style(); const QWidget *widget = option.widget; const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option, widget); if (option.state & QStyle::State_Selected) { /// selected lines are drawn with different color painter->setPen(option.palette.highlightedText().color()); } /// count will be empty unless only one column is shown const QString count = index.column() == 0 && index.model()->columnCount() == 1 ? QString(QStringLiteral(" (%1)")).arg(index.data(ValueListModel::CountRole).toInt()) : QString(); /// squeeze the folder text if it is to big and calculate the rectangles /// where the folder text and the unread count will be drawn to const QFontMetrics fm(painter->fontMetrics()); #if QT_VERSION >= 0x050b00 const int countWidth = fm.horizontalAdvance(count); int fieldWidth = fm.horizontalAdvance(field); #else // QT_VERSION >= 0x050b00 const int countWidth = fm.width(count); int fieldWidth = fm.width(field); #endif // QT_VERSION >= 0x050b00 if (countWidth + fieldWidth > textRect.width()) { /// text plus count is too wide for column, cut text and insert "..." field = fm.elidedText(field, Qt::ElideRight, textRect.width() - countWidth - 8); fieldWidth = textRect.width() - countWidth - 12; } /// determine rects to draw field int top = textRect.top() + (textRect.height() - fm.height()) / 2; QRect fieldRect = textRect; QRect countRect = textRect; fieldRect.setTop(top); fieldRect.setHeight(fm.height()); if (m_parent->header()->visualIndex(index.column()) == 0) { /// left-align text fieldRect.setLeft(fieldRect.left() + 4); ///< hm, indent necessary? fieldRect.setRight(fieldRect.left() + fieldWidth); } else { /// right-align text fieldRect.setRight(fieldRect.right() - 4); ///< hm, indent necessary? fieldRect.setLeft(fieldRect.right() - fieldWidth); ///< hm, indent necessary? } /// draw field name painter->drawText(fieldRect, Qt::AlignLeft, field); if (!count.isEmpty()) { /// determine rects to draw count countRect.setTop(top); countRect.setHeight(fm.height()); countRect.setLeft(fieldRect.right()); /// use bold font QFont font = painter->font(); font.setBold(true); painter->setFont(font); /// determine color for count number const QColor countColor = (option.state & QStyle::State_Selected) ? KColorScheme(QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::LinkText).color() : KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color(); painter->setPen(countColor); /// draw count painter->drawText(countRect, Qt::AlignLeft, count); } /// restore painter's state painter->restore(); } ValueListModel::ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent) - : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortByText) + : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortBy::Text) { readConfiguration(); updateValues(); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); } int ValueListModel::rowCount(const QModelIndex &parent) const { return parent == QModelIndex() ? values.count() : 0; } int ValueListModel::columnCount(const QModelIndex &parent) const { return parent == QModelIndex() ? (showCountColumn ? 2 : 1) : 0; } QVariant ValueListModel::data(const QModelIndex &index, int role) const { if (index.row() >= values.count() || index.column() >= 2) return QVariant(); if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { if (index.column() == 0) { if (fName == Entry::ftColor) { QString text = values[index.row()].text; if (text.isEmpty()) return QVariant(); QString colorText = colorToLabel[text]; if (colorText.isEmpty()) return QVariant(text); return QVariant(colorText); } else return QVariant(values[index.row()].text); } else return QVariant(values[index.row()].count); } else if (role == SortRole) { static const QRegularExpression ignoredInSorting(QStringLiteral("[{}\\\\]+")); QString buffer = values[index.row()].sortBy.isEmpty() ? values[index.row()].text : values[index.row()].sortBy; buffer = buffer.remove(ignoredInSorting).toLower(); - if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortByCount)) { + if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortBy::Count)) { /// Sort by string consisting of a zero-padded count and the lower-case text, /// for example "0000000051keyword" /// Used if (a) two columns are shown (showCountColumn is true) and column 1 /// (the count column) is to be sorted or (b) if only one column is shown /// (showCountColumn is false) and this single column is to be sorted by count. return QString(QStringLiteral("%1%2")).arg(values[index.row()].count, 10, 10, QLatin1Char('0')).arg(buffer); } else { /// Otherwise use lower-case text for sorting return QVariant(buffer); } } else if (role == SearchTextRole) { return QVariant(values[index.row()].text); } else if (role == Qt::EditRole) return QVariant::fromValue(values[index.row()].value); else if (role == CountRole) return QVariant(values[index.row()].count); else return QVariant(); } bool ValueListModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_ASSERT_X(file != nullptr, "ValueListModel::setData", "You cannot set data if there is no BibTeX file associated with this value list."); /// Continue only if in edit role and first column is to be changed if (role == Qt::EditRole && index.column() == 0) { /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Retrieve the Value object containing the user-entered data Value newValue = value.value<Value>(); /// nice variable names ... ;-) if (newValue.isEmpty()) { qCWarning(LOG_KBIBTEX_GUI) << "Cannot replace with empty value"; return false; } /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText == origText) { qCWarning(LOG_KBIBTEX_GUI) << "Skipping to replace value with itself"; return false; } bool success = searchAndReplaceValueInEntries(index, newValue) && searchAndReplaceValueInModel(index, newValue); return success; } return false; } Qt::ItemFlags ValueListModel::flags(const QModelIndex &index) const { Qt::ItemFlags result = QAbstractTableModel::flags(index); /// make first column editable if (index.column() == 0) result |= Qt::ItemIsEditable; return result; } QVariant ValueListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section >= 2 || orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); - else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortByText)) + else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortBy::Text)) return QVariant(i18n("Value")); else return QVariant(i18n("Count")); } void ValueListModel::removeValue(const QModelIndex &index) { removeValueFromEntries(index); removeValueFromModel(index); } void ValueListModel::setShowCountColumn(bool showCountColumn) { beginResetModel(); this->showCountColumn = showCountColumn; endResetModel(); } void ValueListModel::setSortBy(SortBy sortBy) { beginResetModel(); this->sortBy = sortBy; endResetModel(); } void ValueListModel::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) { beginResetModel(); readConfiguration(); endResetModel(); } } void ValueListModel::readConfiguration() { /// load mapping from color value to label colorToLabel.clear(); for (QVector<QPair<QColor, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it) colorToLabel.insert(it->first.name(), it->second); } void ValueListModel::updateValues() { values.clear(); if (file == nullptr) return; for (const auto &element : const_cast<const File &>(*file)) { QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { for (Entry::ConstIterator eit = entry->constBegin(); eit != entry->constEnd(); ++eit) { QString key = eit.key().toLower(); if (key == fName) { insertValue(eit.value()); break; } if (eit.value().isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "value for key" << key << "in entry" << entry->id() << "is empty"; } } } } void ValueListModel::insertValue(const Value &value) { for (const QSharedPointer<ValueItem> &item : value) { const QString text = PlainTextValue::text(*item); if (text.isEmpty()) continue; ///< skip empty values int index = indexOf(text); if (index < 0) { /// previously unknown text ValueLine newValueLine; newValueLine.text = text; newValueLine.count = 1; newValueLine.value.append(item); /// memorize sorting criterium: /// * for persons, use last name first /// * in any case, use lower case const QSharedPointer<Person> person = item.dynamicCast<Person>(); newValueLine.sortBy = person.isNull() ? text.toLower() : person->lastName().toLower() + QStringLiteral(" ") + person->firstName().toLower(); values << newValueLine; } else { ++values[index].count; } } } int ValueListModel::indexOf(const QString &text) { QString color; QString cmpText = text; if (fName == Entry::ftColor && !(color = colorToLabel.key(text, QString())).isEmpty()) cmpText = color; if (cmpText.isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "Should never happen"; int i = 0; /// this is really slow for large data sets: O(n^2) /// maybe use a hash table instead? for (const ValueLine &valueLine : const_cast<const ValueLineList &>(values)) { if (valueLine.text == cmpText) return i; ++i; } return -1; } bool ValueListModel::searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Go through all elements in the current file for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { eit.value().replace(origText, newValue.first()); break; } } } } return true; } bool ValueListModel::searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; const int row = index.row(); /// Test if user-entered text exists already in model's data /// newTextAlreadyInListIndex will be row of duplicate or /// -1 if new text is unique int newTextAlreadyInListIndex = -1; for (int r = values.count() - 1; newTextAlreadyInListIndex < 0 && r >= 0; --r) { if (row != r && values[r].text == newText) newTextAlreadyInListIndex = r; } if (newTextAlreadyInListIndex < 0) { /// User-entered text is unique, so simply replace /// old text with new text values[row].text = newText; values[row].value = newValue; const QSharedPointer<Person> person = newValue.first().dynamicCast<Person>(); values[row].sortBy = person.isNull() ? QString() : person->lastName() + QStringLiteral(" ") + person->firstName(); } else { /// The user-entered text existed before const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } /// Notify Qt about data changed emit dataChanged(index, index); return true; } void ValueListModel::removeValueFromEntries(const QModelIndex &index) { /// Retrieve the Value object containing the user-entered data const Value toBeDeletedValue = values[index.row()].value; if (toBeDeletedValue.isEmpty()) { return; } const QString toBeDeletedText = PlainTextValue::text(toBeDeletedValue); if (toBeDeletedText.isEmpty()) { return; } /// Go through all elements in the current file for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { /// Fetch the key-value pair's value's textual representation const QString valueFullText = PlainTextValue::text(eit.value()); if (valueFullText == toBeDeletedText) { /// If the key-value pair's value's textual representation is the same /// as the value to be delted, remove this key-value pair /// This test is usually true for keys like title, year, or edition. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } else { /// The test above failed, but the delete operation may have /// to be applied to a ValueItem inside the value. /// Possible keys for such a case include author, editor, or keywords. /// Process each ValueItem inside this Value for (Value::Iterator vit = eit.value().begin(); vit != eit.value().end();) { /// Similar procedure as for full values above: /// If a ValueItem's textual representation is the same /// as the shown string which has be deleted, remove the /// ValueItem from this Value. If the Value becomes empty, /// remove Value as well. const QString valueItemText = PlainTextValue::text(* (*vit)); if (valueItemText == toBeDeletedText) { /// Erase old ValueItem from this Value vit = eit.value().erase(vit); } else ++vit; } if (eit.value().isEmpty()) { /// This value does no longer contain any ValueItems. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } } break; } } } } } void ValueListModel::removeValueFromModel(const QModelIndex &index) { const int row = index.row(); const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; emit dataChanged(index, index); } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } diff --git a/src/gui/valuelistmodel.h b/src/gui/valuelistmodel.h index 9d8043db..c4746512 100644 --- a/src/gui/valuelistmodel.h +++ b/src/gui/valuelistmodel.h @@ -1,121 +1,121 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_GUI_VALUELISTMODEL_H #define KBIBTEX_GUI_VALUELISTMODEL_H #include <QAbstractTableModel> #include <QTreeView> #include <QStyledItemDelegate> #include <NotificationHub> #include <Value> #include <models/FileModel> #include "kbibtexgui_export.h" class KBIBTEXGUI_EXPORT ValueListDelegate : public QStyledItemDelegate { Q_OBJECT private: QString m_fieldName; QTreeView *m_parent; public: explicit ValueListDelegate(QTreeView *parent = nullptr) : QStyledItemDelegate(parent), m_fieldName(QString()), m_parent(parent) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setFieldName(const QString &fieldName) { m_fieldName = fieldName; } private slots: void commitAndCloseEditor(); }; class KBIBTEXGUI_EXPORT ValueListModel : public QAbstractTableModel, private NotificationListener { Q_OBJECT public: enum ValueListModelRole { /// How many occurrences a value has CountRole = Qt::UserRole + 112, /// Role to sort values by SortRole = Qt::UserRole + 113, /// Role to get text to filter for SearchTextRole = Qt::UserRole + 114 }; - enum SortBy { SortByText, SortByCount }; + enum class SortBy { Text, Count }; private: struct ValueLine { QString text; QString sortBy; Value value; int count; }; typedef QVector<ValueLine> ValueLineList; const File *file; const QString fName; ValueLineList values; QMap<QString, QString> colorToLabel; bool showCountColumn; SortBy sortBy; public: ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void removeValue(const QModelIndex &index); void setShowCountColumn(bool showCountColumn); void setSortBy(SortBy sortBy); void notificationEvent(int eventId) override; private: void readConfiguration(); void updateValues(); void insertValue(const Value &value); int indexOf(const QString &text); QString htmlize(const QString &text) const; bool searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue); bool searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue); void removeValueFromEntries(const QModelIndex &index); void removeValueFromModel(const QModelIndex &index); }; #endif // KBIBTEX_GUI_VALUELISTMODEL_H diff --git a/src/gui/widgets/filterbar.cpp b/src/gui/widgets/filterbar.cpp index 3d689bbd..32131ff3 100644 --- a/src/gui/widgets/filterbar.cpp +++ b/src/gui/widgets/filterbar.cpp @@ -1,328 +1,328 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "filterbar.h" #include <algorithm> #include <QLayout> #include <QLabel> #include <QTimer> #include <QIcon> #include <QPushButton> #include <QLineEdit> #include <QComboBox> #include <KLocalizedString> #include <KConfigGroup> #include <KSharedConfig> #include <BibTeXFields> #include "delayedexecutiontimer.h" static bool sortStringsLocaleAware(const QString &s1, const QString &s2) { return QString::localeAwareCompare(s1, s2) < 0; } class FilterBar::FilterBarPrivate { private: FilterBar *p; public: KSharedConfigPtr config; const QString configGroupName; QComboBox *comboBoxFilterText; const int maxNumStoredFilterTexts; QComboBox *comboBoxCombination; QComboBox *comboBoxField; QPushButton *buttonSearchPDFfiles; QPushButton *buttonClearAll; DelayedExecutionTimer *delayedTimer; FilterBarPrivate(FilterBar *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Filter Bar")), maxNumStoredFilterTexts(12) { delayedTimer = new DelayedExecutionTimer(p); setupGUI(); connect(delayedTimer, &DelayedExecutionTimer::triggered, p, &FilterBar::publishFilter); } ~FilterBarPrivate() { delete delayedTimer; } void setupGUI() { QBoxLayout *layout = new QHBoxLayout(p); layout->setMargin(0); QLabel *label = new QLabel(i18n("Filter:"), p); layout->addWidget(label, 0); comboBoxFilterText = new QComboBox(p); label->setBuddy(comboBoxFilterText); layout->addWidget(comboBoxFilterText, 5); comboBoxFilterText->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); comboBoxFilterText->setEditable(true); QFontMetrics metrics(comboBoxFilterText->font()); #if QT_VERSION >= 0x050b00 comboBoxFilterText->setMinimumWidth(metrics.horizontalAdvance(QStringLiteral("AIWaiw")) * 7); #else // QT_VERSION >= 0x050b00 comboBoxFilterText->setMinimumWidth(metrics.width(QStringLiteral("AIWaiw")) * 7); #endif // QT_VERSION >= 0x050b00 QLineEdit *lineEdit = static_cast<QLineEdit *>(comboBoxFilterText->lineEdit()); lineEdit->setClearButtonEnabled(true); lineEdit->setPlaceholderText(i18n("Filter bibliographic entries")); comboBoxCombination = new QComboBox(p); layout->addWidget(comboBoxCombination, 1); comboBoxCombination->addItem(i18n("any word")); /// AnyWord=0 comboBoxCombination->addItem(i18n("every word")); /// EveryWord=1 comboBoxCombination->addItem(i18n("exact phrase")); /// ExactPhrase=2 comboBoxCombination->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); comboBoxField = new QComboBox(p); layout->addWidget(comboBoxField, 1); comboBoxField->addItem(i18n("any field"), QVariant()); comboBoxField->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); /// Use a hash map to get an alphabetically sorted list QHash<QString, QString> fielddescs; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) if (fd.upperCamelCaseAlt.isEmpty()) fielddescs.insert(fd.label, fd.upperCamelCase); /// Sort locale-aware QList<QString> keys = fielddescs.keys(); std::sort(keys.begin(), keys.end(), sortStringsLocaleAware); for (const QString &key : const_cast<const QList<QString> &>(keys)) { const QString &value = fielddescs[key]; comboBoxField->addItem(key, value); } buttonSearchPDFfiles = new QPushButton(p); buttonSearchPDFfiles->setIcon(QIcon::fromTheme(QStringLiteral("application-pdf"))); buttonSearchPDFfiles->setToolTip(i18n("Include PDF files in full-text search")); buttonSearchPDFfiles->setCheckable(true); layout->addWidget(buttonSearchPDFfiles, 0); buttonClearAll = new QPushButton(p); buttonClearAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl"))); buttonClearAll->setToolTip(i18n("Reset filter criteria")); layout->addWidget(buttonClearAll, 0); /// restore history on filter texts /// see addCompletionString for more detailed explanation KConfigGroup configGroup(config, configGroupName); QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end(); ++it) comboBoxFilterText->addItem((*it).mid(12)); comboBoxFilterText->lineEdit()->setText(QString()); comboBoxCombination->setCurrentIndex(configGroup.readEntry("CurrentCombination", 0)); comboBoxField->setCurrentIndex(configGroup.readEntry("CurrentField", 0)); connect(comboBoxFilterText->lineEdit(), &QLineEdit::textChanged, delayedTimer, &DelayedExecutionTimer::trigger); connect(comboBoxFilterText->lineEdit(), &QLineEdit::returnPressed, p, &FilterBar::userPressedEnter); connect(comboBoxCombination, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); connect(comboBoxField, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &FilterBar::comboboxStatusChanged); connect(buttonSearchPDFfiles, &QPushButton::toggled, p, &FilterBar::comboboxStatusChanged); connect(comboBoxCombination, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), delayedTimer, &DelayedExecutionTimer::trigger); connect(comboBoxField, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), delayedTimer, &DelayedExecutionTimer::trigger); connect(buttonSearchPDFfiles, &QPushButton::toggled, delayedTimer, &DelayedExecutionTimer::trigger); connect(buttonClearAll, &QPushButton::clicked, p, &FilterBar::resetState); } SortFilterFileModel::FilterQuery filter() { SortFilterFileModel::FilterQuery result; - result.combination = comboBoxCombination->currentIndex() == 0 ? SortFilterFileModel::AnyTerm : SortFilterFileModel::EveryTerm; + result.combination = comboBoxCombination->currentIndex() == 0 ? SortFilterFileModel::FilterCombination::AnyTerm : SortFilterFileModel::FilterCombination::EveryTerm; result.terms.clear(); if (comboBoxCombination->currentIndex() == 2) /// exact phrase result.terms << comboBoxFilterText->lineEdit()->text(); else { /// any or every word static const QRegularExpression sequenceOfSpacesRegExp(QStringLiteral("\\s+")); result.terms = comboBoxFilterText->lineEdit()->text().split(sequenceOfSpacesRegExp, QString::SkipEmptyParts); } result.field = comboBoxField->currentIndex() == 0 ? QString() : comboBoxField->itemData(comboBoxField->currentIndex(), Qt::UserRole).toString(); result.searchPDFfiles = buttonSearchPDFfiles->isChecked(); return result; } void setFilter(const SortFilterFileModel::FilterQuery &fq) { /// Avoid triggering loops of activation comboBoxCombination->blockSignals(true); /// Set check state for action for either "any word", /// "every word", or "exact phrase", respectively - const int combinationIndex = fq.combination == SortFilterFileModel::AnyTerm ? 0 : (fq.terms.count() < 2 ? 2 : 1); + const int combinationIndex = fq.combination == SortFilterFileModel::FilterCombination::AnyTerm ? 0 : (fq.terms.count() < 2 ? 2 : 1); comboBoxCombination->setCurrentIndex(combinationIndex); /// Reset activation block comboBoxCombination->blockSignals(false); /// Avoid triggering loops of activation comboBoxField->blockSignals(true); /// Find and check action that corresponds to field name ("author", ...) const QString lower = fq.field.toLower(); for (int idx = comboBoxField->count() - 1; idx >= 0; --idx) { if (comboBoxField->itemData(idx, Qt::UserRole).toString().toLower() == lower) { comboBoxField->setCurrentIndex(idx); break; } } /// Reset activation block comboBoxField->blockSignals(false); /// Avoid triggering loops of activation buttonSearchPDFfiles->blockSignals(true); /// Set flag if associated PDF files have to be searched buttonSearchPDFfiles->setChecked(fq.searchPDFfiles); /// Reset activation block buttonSearchPDFfiles->blockSignals(false); /// Avoid triggering loops of activation comboBoxFilterText->lineEdit()->blockSignals(true); /// Set filter text widget's content comboBoxFilterText->lineEdit()->setText(fq.terms.join(QStringLiteral(" "))); /// Reset activation block comboBoxFilterText->lineEdit()->blockSignals(false); } bool modelContainsText(QAbstractItemModel *model, const QString &text) { for (int row = 0; row < model->rowCount(); ++row) if (model->index(row, 0, QModelIndex()).data().toString().contains(text)) return true; return false; } void addCompletionString(const QString &text) { KConfigGroup configGroup(config, configGroupName); /// Previous searches are stored as a string list, where each individual /// string starts with 12 characters for the date and time when this /// search was used. Starting from the 13th character (12th, if you /// start counting from 0) the user's input is stored. /// This approach has several advantages: It does not require a more /// complex data structure, can easily read and written using /// KConfigGroup's functions, and can be sorted lexicographically/ /// chronologically using QStringList's sort. /// Disadvantage is that string fragments have to be managed manually. QStringList completionListDate = configGroup.readEntry(QStringLiteral("PreviousSearches"), QStringList()); for (QStringList::Iterator it = completionListDate.begin(); it != completionListDate.end();) if ((*it).mid(12) == text) it = completionListDate.erase(it); else ++it; completionListDate << (QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMddhhmm")) + text); /// after sorting, discard all but the maxNumStoredFilterTexts most /// recent user-entered filter texts completionListDate.sort(); while (completionListDate.count() > maxNumStoredFilterTexts) completionListDate.removeFirst(); configGroup.writeEntry(QStringLiteral("PreviousSearches"), completionListDate); config->sync(); /// add user-entered filter text to combobox's drop-down list if (!text.isEmpty() && !modelContainsText(comboBoxFilterText->model(), text)) comboBoxFilterText->addItem(text); } void storeComboBoxStatus() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(QStringLiteral("CurrentCombination"), comboBoxCombination->currentIndex()); configGroup.writeEntry(QStringLiteral("CurrentField"), comboBoxField->currentIndex()); configGroup.writeEntry(QStringLiteral("SearchPDFFiles"), buttonSearchPDFfiles->isChecked()); config->sync(); } void restoreState() { KConfigGroup configGroup(config, configGroupName); comboBoxCombination->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentCombination"), 0)); comboBoxField->setCurrentIndex(configGroup.readEntry(QStringLiteral("CurrentField"), 0)); buttonSearchPDFfiles->setChecked(configGroup.readEntry(QStringLiteral("SearchPDFFiles"), false)); } void resetState() { comboBoxFilterText->lineEdit()->clear(); comboBoxCombination->setCurrentIndex(0); comboBoxField->setCurrentIndex(0); buttonSearchPDFfiles->setChecked(false); } }; FilterBar::FilterBar(QWidget *parent) : QWidget(parent), d(new FilterBarPrivate(this)) { d->restoreState(); setFocusProxy(d->comboBoxFilterText); QTimer::singleShot(250, this, &FilterBar::buttonHeight); } FilterBar::~FilterBar() { delete d; } void FilterBar::setFilter(const SortFilterFileModel::FilterQuery &fq) { d->setFilter(fq); emit filterChanged(fq); } SortFilterFileModel::FilterQuery FilterBar::filter() { return d->filter(); } void FilterBar::setPlaceholderText(const QString &msg) { QLineEdit *lineEdit = static_cast<QLineEdit *>(d->comboBoxFilterText->lineEdit()); lineEdit->setPlaceholderText(msg); } void FilterBar::comboboxStatusChanged() { d->buttonSearchPDFfiles->setEnabled(d->comboBoxField->currentIndex() == 0); d->storeComboBoxStatus(); } void FilterBar::resetState() { d->resetState(); emit filterChanged(d->filter()); } void FilterBar::userPressedEnter() { /// only store text in auto-completion if user pressed enter d->addCompletionString(d->comboBoxFilterText->lineEdit()->text()); publishFilter(); } void FilterBar::publishFilter() { emit filterChanged(d->filter()); } void FilterBar::buttonHeight() { QSizePolicy sp = d->buttonSearchPDFfiles->sizePolicy(); d->buttonSearchPDFfiles->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); d->buttonClearAll->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding); } diff --git a/src/io/bibutils.cpp b/src/io/bibutils.cpp index d1f8b51c..02a26e1c 100644 --- a/src/io/bibutils.cpp +++ b/src/io/bibutils.cpp @@ -1,189 +1,210 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "bibutils.h" #include <QProcess> #include <QBuffer> #include <QByteArray> #include <QStandardPaths> #include "logging_io.h" class BibUtils::Private { public: BibUtils::Format format; Private(BibUtils *parent) - : format(BibUtils::MODS) + : format(BibUtils::Format::MODS) { Q_UNUSED(parent) } }; BibUtils::BibUtils() : d(new BibUtils::Private(this)) { /// nothing } BibUtils::~BibUtils() { delete d; } void BibUtils::setFormat(const BibUtils::Format format) { d->format = format; } BibUtils::Format BibUtils::format() const { return d->format; } bool BibUtils::available() { enum State {untested, avail, unavail}; static State state = untested; /// Perform test only once, later rely on statically stored result if (state == untested) { /// Test a number of known BibUtils programs static const QStringList programs {QStringLiteral("bib2xml"), QStringLiteral("isi2xml"), QStringLiteral("ris2xml"), QStringLiteral("end2xml")}; state = avail; for (const QString &program : programs) { const QString fullPath = QStandardPaths::findExecutable(program); if (fullPath.isEmpty()) { state = unavail; ///< missing a single program is reason to assume that BibUtils is not correctly installed break; } } if (state == avail) qCDebug(LOG_KBIBTEX_IO) << "BibUtils found, using it to import/export certain types of bibliographies"; else if (state == unavail) qCWarning(LOG_KBIBTEX_IO) << "No or only an incomplete installation of BibUtils found"; } return state == avail; } bool BibUtils::convert(QIODevice &source, const BibUtils::Format sourceFormat, QIODevice &destination, const BibUtils::Format destinationFormat) const { /// To proceed, either the source format or the destination format /// has to be MODS, otherwise ... - if (sourceFormat != MODS && destinationFormat != MODS) { + if (sourceFormat != BibUtils::Format::MODS && destinationFormat != BibUtils::Format::MODS) { /// Add indirection: convert source format to MODS, /// then convert MODS data to destination format /// Intermediate buffer to hold MODS data QBuffer buffer; - bool result = convert(source, sourceFormat, buffer, BibUtils::MODS); + bool result = convert(source, sourceFormat, buffer, BibUtils::Format::MODS); if (result) - result = convert(buffer, BibUtils::MODS, destination, destinationFormat); + result = convert(buffer, BibUtils::Format::MODS, destination, destinationFormat); return result; } QString bibUtilsProgram; QString utf8Argument = QStringLiteral("-un"); /// Determine part of BibUtils program name that represents source format switch (sourceFormat) { - case MODS: bibUtilsProgram = QStringLiteral("xml"); utf8Argument = QStringLiteral("-nb"); break; - case BibTeX: bibUtilsProgram = QStringLiteral("bib"); break; - case BibLaTeX: bibUtilsProgram = QStringLiteral("biblatex"); break; - case ISI: bibUtilsProgram = QStringLiteral("isi"); break; - case RIS: bibUtilsProgram = QStringLiteral("ris"); break; - case EndNote: bibUtilsProgram = QStringLiteral("end"); break; - case EndNoteXML: bibUtilsProgram = QStringLiteral("endx"); break; + case BibUtils::Format::MODS: bibUtilsProgram = QStringLiteral("xml"); utf8Argument = QStringLiteral("-nb"); break; + case BibUtils::Format::BibTeX: bibUtilsProgram = QStringLiteral("bib"); break; + case BibUtils::Format::BibLaTeX: bibUtilsProgram = QStringLiteral("biblatex"); break; + case BibUtils::Format::ISI: bibUtilsProgram = QStringLiteral("isi"); break; + case BibUtils::Format::RIS: bibUtilsProgram = QStringLiteral("ris"); break; + case BibUtils::Format::EndNote: bibUtilsProgram = QStringLiteral("end"); break; + case BibUtils::Format::EndNoteXML: bibUtilsProgram = QStringLiteral("endx"); break; /// case ADS not supported by BibUtils - case WordBib: bibUtilsProgram = QStringLiteral("wordbib"); break; - case Copac: bibUtilsProgram = QStringLiteral("copac"); break; - case Med: bibUtilsProgram = QStringLiteral("med"); break; + case BibUtils::Format::WordBib: bibUtilsProgram = QStringLiteral("wordbib"); break; + case BibUtils::Format::Copac: bibUtilsProgram = QStringLiteral("copac"); break; + case BibUtils::Format::Med: bibUtilsProgram = QStringLiteral("med"); break; default: qCWarning(LOG_KBIBTEX_IO) << "Unsupported BibUtils input format:" << sourceFormat; return false; } bibUtilsProgram.append(QStringLiteral("2")); /// Determine part of BibUtils program name that represents destination format switch (destinationFormat) { - case MODS: bibUtilsProgram.append(QStringLiteral("xml")); break; - case BibTeX: bibUtilsProgram.append(QStringLiteral("bib")); break; + case BibUtils::Format::MODS: bibUtilsProgram.append(QStringLiteral("xml")); break; + case BibUtils::Format::BibTeX: bibUtilsProgram.append(QStringLiteral("bib")); break; /// case BibLaTeX not supported by BibUtils - case ISI: bibUtilsProgram.append(QStringLiteral("isi")); break; - case RIS: bibUtilsProgram.append(QStringLiteral("ris")); break; - case EndNote: bibUtilsProgram.append(QStringLiteral("end")); break; + case BibUtils::Format::ISI: bibUtilsProgram.append(QStringLiteral("isi")); break; + case BibUtils::Format::RIS: bibUtilsProgram.append(QStringLiteral("ris")); break; + case BibUtils::Format::EndNote: bibUtilsProgram.append(QStringLiteral("end")); break; /// case EndNoteXML not supported by BibUtils - case ADS: bibUtilsProgram.append(QStringLiteral("ads")); break; - case WordBib: bibUtilsProgram.append(QStringLiteral("wordbib")); break; + case BibUtils::Format::ADS: bibUtilsProgram.append(QStringLiteral("ads")); break; + case BibUtils::Format::WordBib: bibUtilsProgram.append(QStringLiteral("wordbib")); break; /// case Copac not supported by BibUtils /// case Med not supported by BibUtils default: qCWarning(LOG_KBIBTEX_IO) << "Unsupported BibUtils output format:" << destinationFormat; return false; } /// Test if required BibUtils program is available bibUtilsProgram = QStandardPaths::findExecutable(bibUtilsProgram); if (bibUtilsProgram.isEmpty()) return false; /// Test if source device is readable if (!source.isReadable() && !source.open(QIODevice::ReadOnly)) return false; /// Test if destination device is writable if (!destination.isWritable() && !destination.open(QIODevice::WriteOnly)) { source.close(); return false; } QProcess bibUtilsProcess; const QStringList arguments {QStringLiteral("-i"), QStringLiteral("utf8"), utf8Argument}; /// Start BibUtils program/process bibUtilsProcess.start(bibUtilsProgram, arguments); bool result = bibUtilsProcess.waitForStarted(); if (result) { /// Write source data to process's stdin bibUtilsProcess.write(source.readAll()); /// Close process's stdin start transformation bibUtilsProcess.closeWriteChannel(); result = bibUtilsProcess.waitForFinished(); /// If process run without problems ... if (result && bibUtilsProcess.exitStatus() == QProcess::NormalExit) { /// Read process's output, i.e. the transformed data const QByteArray stdOut = bibUtilsProcess.readAllStandardOutput(); if (!stdOut.isEmpty()) { /// Write transformed data to destination device const int amountWritten = destination.write(stdOut); /// Check that the same amount of bytes is written /// as received from the BibUtils program result = amountWritten == stdOut.size(); } else result = false; } else result = false; } /// In case it did not terminate earlier bibUtilsProcess.terminate(); /// Close both source and destination device source.close(); destination.close(); return result; } + +QDebug operator<<(QDebug dbg, const BibUtils::Format &format) +{ + static const auto pairs = QHash<int, const char *> { + {static_cast<int>(BibUtils::Format::MODS), "MODS"}, + {static_cast<int>(BibUtils::Format::BibTeX), "BibTeX"}, + {static_cast<int>(BibUtils::Format::BibLaTeX), "BibLaTeX"}, + {static_cast<int>(BibUtils::Format::ISI), "ISI"}, + {static_cast<int>(BibUtils::Format::RIS), "RIS"}, + {static_cast<int>(BibUtils::Format::EndNote), "EndNote"}, + {static_cast<int>(BibUtils::Format::EndNoteXML), "EndNoteXML"}, + {static_cast<int>(BibUtils::Format::ADS), "ADS"}, + {static_cast<int>(BibUtils::Format::WordBib), "WordBib"}, + {static_cast<int>(BibUtils::Format::Copac), "Copac"}, + {static_cast<int>(BibUtils::Format::Med), "Med"} + }; + dbg.nospace(); + const int formatInt = static_cast<int>(format); + dbg << (pairs.contains(formatInt) ? pairs[formatInt] : "???"); + return dbg; +} diff --git a/src/io/bibutils.h b/src/io/bibutils.h index 670f4c79..2ad6ade5 100644 --- a/src/io/bibutils.h +++ b/src/io/bibutils.h @@ -1,69 +1,71 @@ /*************************************************************************** - * Copyright (C) 2004-2014 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_BIBUTILS_H #define KBIBTEX_IO_BIBUTILS_H #include <QIODevice> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 /** * This class encapsulates calling the various binary programs of the BibUtils program set. * BibUtils is available at http://sourceforge.net/projects/bibutils/ * * This class is inherited by @see FileImporterBibUtils and @see FileExporterBibUtils, * which make use of its protected functions. * Using this class directly should only happen to call its public static functions. * * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXIO_EXPORT BibUtils { public: BibUtils(const BibUtils &other) = delete; BibUtils &operator= (const BibUtils &other) = delete; ~BibUtils(); - enum Format { MODS = 0, BibTeX = 1, BibLaTeX = 2, ISI = 5, RIS = 6, EndNote = 10, EndNoteXML = 11, ADS = 15, WordBib = 16, Copac = 17, Med = 18 }; + enum class Format { MODS = 0, BibTeX = 1, BibLaTeX = 2, ISI = 5, RIS = 6, EndNote = 10, EndNoteXML = 11, ADS = 15, WordBib = 16, Copac = 17, Med = 18 }; BibUtils::Format format() const; void setFormat(const BibUtils::Format format); /** * Test if BibUtils is installed. This test checks if a number of known * BibUtils binaries are available (i.e. found in PATH). If any binary * is missing, it is assumed that BibUtils is not available. The test is * performed only once and the result cached for future calls to this function. * @return true if BibUtils is correctly installed, false otherwise */ static bool available(); protected: explicit BibUtils(); // TODO migrate to KJob or KCompositeJob bool convert(QIODevice &source, const BibUtils::Format sourceFormat, QIODevice &destination, const BibUtils::Format destinationFormat) const; private: class Private; Private *const d; }; +KBIBTEXIO_EXPORT QDebug operator<<(QDebug dbg, const BibUtils::Format &format); + #endif // KBIBTEX_IO_BIBUTILS_H diff --git a/src/io/encoder.h b/src/io/encoder.h index fc906e02..21e5256b 100644 --- a/src/io/encoder.h +++ b/src/io/encoder.h @@ -1,71 +1,71 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_ENCODER_H #define KBIBTEX_IO_ENCODER_H #include <QString> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 /** * Base class for that convert between different textual representations * for non-ASCII characters. Examples for external textual representations * are \"a in LaTeX and &auml; in XML. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXIO_EXPORT Encoder { public: - enum TargetEncoding {TargetEncodingASCII = 0, TargetEncodingUTF8 = 1}; + enum class TargetEncoding {ASCII, UTF8}; static const Encoder &instance(); virtual ~Encoder(); /** * Decode from external textual representation to internal (UTF-8) representation. * @param text text in external textual representation * @return text in internal (UTF-8) representation */ virtual QString decode(const QString &text) const; /** * Encode from internal (UTF-8) representation to external textual representation. * Output may be restricted to ASCII (non-ASCII characters will be rewritten depending * on concrete Encoder class, for example as '&#228;' as XML or '\"a' for LaTeX) * or UTF-8 (all characters allowed, only 'special ones' rewritten, for example * '&amp;' for XML and '\&' for LaTeX). * @param text in internal (UTF-8) representation * @param targetEncoding allow either only ASCII output or UTF-8 output. * @return text text in external textual representation */ virtual QString encode(const QString &text, const TargetEncoding targetEncoding) const; QString convertToPlainAscii(const QString &input) const; static bool containsOnlyAscii(const QString &text); protected: Encoder(); private: class Private; Private *const d; }; #endif // KBIBTEX_IO_ENCODER_H diff --git a/src/io/encoderlatex.cpp b/src/io/encoderlatex.cpp index 575d8ffd..0e1255f7 100644 --- a/src/io/encoderlatex.cpp +++ b/src/io/encoderlatex.cpp @@ -1,1463 +1,1463 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "encoderlatex.h" #include <QString> #include "logging_io.h" inline bool isAsciiLetter(const QChar c) { return (c.unicode() >= static_cast<ushort>('A') && c.unicode() <= static_cast<ushort>('Z')) || (c.unicode() >= static_cast<ushort>('a') && c.unicode() <= static_cast<ushort>('z')); } inline int asciiLetterOrDigitToPos(const QChar c) { static const ushort upperCaseLetterA = QLatin1Char('A').unicode(); static const ushort upperCaseLetterZ = QLatin1Char('Z').unicode(); static const ushort lowerCaseLetterA = QLatin1Char('a').unicode(); static const ushort lowerCaseLetterZ = QLatin1Char('z').unicode(); static const ushort digit0 = QLatin1Char('0').unicode(); static const ushort digit9 = QLatin1Char('9').unicode(); const ushort unicode = c.unicode(); if (unicode >= upperCaseLetterA && unicode <= upperCaseLetterZ) return unicode - upperCaseLetterA; else if (unicode >= lowerCaseLetterA && unicode <= lowerCaseLetterZ) return unicode + 26 - lowerCaseLetterA; else if (unicode >= digit0 && unicode <= digit9) return unicode + 52 - digit0; else return -1; } inline bool isIJ(const QChar c) { static const QChar upperCaseLetterI = QLatin1Char('I'); static const QChar upperCaseLetterJ = QLatin1Char('J'); static const QChar lowerCaseLetterI = QLatin1Char('i'); static const QChar lowerCaseLetterJ = QLatin1Char('j'); return c == upperCaseLetterI || c == upperCaseLetterJ || c == lowerCaseLetterI || c == lowerCaseLetterJ; } enum EncoderLaTeXCommandDirection { DirectionCommandToUnicode = 1, DirectionUnicodeToCommand = 2, DirectionBoth = DirectionCommandToUnicode | DirectionUnicodeToCommand }; /** * General documentation on this topic: * http://www.tex.ac.uk/CTAN/macros/latex/doc/encguide.pdf * https://mirror.hmc.edu/ctan/macros/xetex/latex/xecjk/xunicode-symbols.pdf * ftp://ftp.dante.de/tex-archive/biblio/biber/documentation/utf8-macro-map.html */ /** * This structure contains information how escaped characters * such as \"a are translated to an Unicode character and back. * The structure is a table with three columns: (1) the modifier * (in the example before the quotation mark) (2) the ASCII * character ((in the example before the 'a') (3) the Unicode * character described by a hexcode. * This data structure is used both directly and indirectly via * the LookupTable structure which is initialized when the * EncoderLaTeX object is created. */ static const struct EncoderLaTeXEscapedCharacter { const QChar modifier; const QChar letter; const ushort unicode; const EncoderLaTeXCommandDirection direction; } encoderLaTeXEscapedCharacters[] = { {QLatin1Char('`'), QLatin1Char('A'), 0x00C0, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('A'), 0x00C1, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('A'), 0x00C2, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('A'), 0x00C3, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('A'), 0x00C4, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('A'), 0x00C5, DirectionBoth}, /** 0x00C6: see EncoderLaTeXCharacterCommand */ {QLatin1Char('c'), QLatin1Char('C'), 0x00C7, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('E'), 0x00C8, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('E'), 0x00C9, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('E'), 0x00CA, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('E'), 0x00CB, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('I'), 0x00CC, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('I'), 0x00CD, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('I'), 0x00CE, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('I'), 0x00CF, DirectionBoth}, /** 0x00D0: see EncoderLaTeXCharacterCommand */ {QLatin1Char('~'), QLatin1Char('N'), 0x00D1, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('O'), 0x00D2, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('O'), 0x00D3, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('O'), 0x00D4, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('O'), 0x00D5, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('O'), 0x00D6, DirectionBoth}, /** 0x00D7: see EncoderLaTeXCharacterCommand */ /** 0x00D8: see EncoderLaTeXCharacterCommand */ {QLatin1Char('`'), QLatin1Char('U'), 0x00D9, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('U'), 0x00DA, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('U'), 0x00DB, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('U'), 0x00DC, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('Y'), 0x00DD, DirectionBoth}, /** 0x00DE: see EncoderLaTeXCharacterCommand */ {QLatin1Char('"'), QLatin1Char('s'), 0x00DF, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('a'), 0x00E0, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('a'), 0x00E1, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('a'), 0x00E2, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('a'), 0x00E3, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('a'), 0x00E4, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('a'), 0x00E5, DirectionBoth}, /** 0x00E6: see EncoderLaTeXCharacterCommand */ {QLatin1Char('c'), QLatin1Char('c'), 0x00E7, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('e'), 0x00E8, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('e'), 0x00E9, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('e'), 0x00EA, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('e'), 0x00EB, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('i'), 0x00EC, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('i'), 0x00ED, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('i'), 0x00EE, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('i'), 0x00EF, DirectionBoth}, /** 0x00F0: see EncoderLaTeXCharacterCommand */ {QLatin1Char('~'), QLatin1Char('n'), 0x00F1, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('o'), 0x00F2, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('o'), 0x00F3, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('o'), 0x00F4, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('o'), 0x00F5, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('o'), 0x00F6, DirectionBoth}, /** 0x00F7: see EncoderLaTeXCharacterCommand */ /** 0x00F8: see EncoderLaTeXCharacterCommand */ {QLatin1Char('`'), QLatin1Char('u'), 0x00F9, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('u'), 0x00FA, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('u'), 0x00FB, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('u'), 0x00FC, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('y'), 0x00FD, DirectionBoth}, /** 0x00FE: see EncoderLaTeXCharacterCommand */ {QLatin1Char('"'), QLatin1Char('y'), 0x00FF, DirectionBoth}, {QLatin1Char('='), QLatin1Char('A'), 0x0100, DirectionBoth}, {QLatin1Char('='), QLatin1Char('a'), 0x0101, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('A'), 0x0102, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('a'), 0x0103, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('A'), 0x0104, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('a'), 0x0105, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('C'), 0x0106, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('c'), 0x0107, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('C'), 0x0108, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('c'), 0x0109, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('C'), 0x010A, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('c'), 0x010B, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('C'), 0x010C, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('c'), 0x010D, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('D'), 0x010E, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('d'), 0x010F, DirectionBoth}, {QLatin1Char('B'), QLatin1Char('D'), 0x0110, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('d'), 0x0111, DirectionCommandToUnicode}, {QLatin1Char('='), QLatin1Char('E'), 0x0112, DirectionBoth}, {QLatin1Char('='), QLatin1Char('e'), 0x0113, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('E'), 0x0114, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('e'), 0x0115, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('E'), 0x0116, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('e'), 0x0117, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('E'), 0x0118, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('e'), 0x0119, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('E'), 0x011A, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('e'), 0x011B, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('G'), 0x011C, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('g'), 0x011D, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('G'), 0x011E, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('g'), 0x011F, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('G'), 0x0120, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('g'), 0x0121, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('G'), 0x0122, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('g'), 0x0123, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('H'), 0x0124, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('h'), 0x0125, DirectionBoth}, {QLatin1Char('B'), QLatin1Char('H'), 0x0126, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('h'), 0x0127, DirectionCommandToUnicode}, {QLatin1Char('~'), QLatin1Char('I'), 0x0128, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('i'), 0x0129, DirectionBoth}, {QLatin1Char('='), QLatin1Char('I'), 0x012A, DirectionBoth}, {QLatin1Char('='), QLatin1Char('i'), 0x012B, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('I'), 0x012C, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('i'), 0x012D, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('I'), 0x012E, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('i'), 0x012F, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('I'), 0x0130, DirectionBoth}, /** 0x0131: see EncoderLaTeXCharacterCommand */ /** 0x0132: see EncoderLaTeXCharacterCommand */ /** 0x0133: see EncoderLaTeXCharacterCommand */ {QLatin1Char('^'), QLatin1Char('J'), 0x012E, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('j'), 0x012F, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('K'), 0x0136, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('k'), 0x0137, DirectionBoth}, /** 0x0138: see EncoderLaTeXCharacterCommand */ {QLatin1Char('\''), QLatin1Char('L'), 0x0139, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('l'), 0x013A, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('L'), 0x013B, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('l'), 0x013C, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('L'), 0x013D, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('l'), 0x013E, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('L'), 0x013F, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('l'), 0x0140, DirectionBoth}, {QLatin1Char('B'), QLatin1Char('L'), 0x0141, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('l'), 0x0142, DirectionCommandToUnicode}, {QLatin1Char('\''), QLatin1Char('N'), 0x0143, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('n'), 0x0144, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('n'), 0x0145, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('n'), 0x0146, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('N'), 0x0147, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('n'), 0x0148, DirectionBoth}, /** 0x0149: TODO n preceded by apostrophe */ {QLatin1Char('m'), QLatin1Char('N'), 0x014A, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('n'), 0x014B, DirectionCommandToUnicode}, {QLatin1Char('='), QLatin1Char('O'), 0x014C, DirectionBoth}, {QLatin1Char('='), QLatin1Char('o'), 0x014D, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('O'), 0x014E, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('o'), 0x014F, DirectionBoth}, {QLatin1Char('H'), QLatin1Char('O'), 0x0150, DirectionBoth}, {QLatin1Char('H'), QLatin1Char('o'), 0x0151, DirectionBoth}, /** 0x0152: see EncoderLaTeXCharacterCommand */ /** 0x0153: see EncoderLaTeXCharacterCommand */ {QLatin1Char('\''), QLatin1Char('R'), 0x0154, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('r'), 0x0155, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('R'), 0x0156, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('r'), 0x0157, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('R'), 0x0158, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('r'), 0x0159, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('S'), 0x015A, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('s'), 0x015B, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('S'), 0x015C, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('s'), 0x015D, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('S'), 0x015E, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('s'), 0x015F, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('S'), 0x0160, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('s'), 0x0161, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('T'), 0x0162, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('t'), 0x0163, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('T'), 0x0164, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('t'), 0x0165, DirectionBoth}, {QLatin1Char('B'), QLatin1Char('T'), 0x0166, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('t'), 0x0167, DirectionCommandToUnicode}, {QLatin1Char('~'), QLatin1Char('U'), 0x0168, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('u'), 0x0169, DirectionBoth}, {QLatin1Char('='), QLatin1Char('U'), 0x016A, DirectionBoth}, {QLatin1Char('='), QLatin1Char('u'), 0x016B, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('U'), 0x016C, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('u'), 0x016D, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('U'), 0x016E, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('u'), 0x016F, DirectionBoth}, {QLatin1Char('H'), QLatin1Char('U'), 0x0170, DirectionBoth}, {QLatin1Char('H'), QLatin1Char('u'), 0x0171, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('U'), 0x0172, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('u'), 0x0173, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('W'), 0x0174, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('w'), 0x0175, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('Y'), 0x0176, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('y'), 0x0177, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('Y'), 0x0178, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('Z'), 0x0179, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('z'), 0x017A, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('Z'), 0x017B, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('z'), 0x017C, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('Z'), 0x017D, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('z'), 0x017E, DirectionBoth}, /** 0x017F: TODO long s */ {QLatin1Char('B'), QLatin1Char('b'), 0x0180, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('B'), 0x0181, DirectionCommandToUnicode}, /** 0x0182 */ /** 0x0183 */ /** 0x0184 */ /** 0x0185 */ {QLatin1Char('m'), QLatin1Char('O'), 0x0186, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('C'), 0x0187, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('c'), 0x0188, DirectionCommandToUnicode}, {QLatin1Char('M'), QLatin1Char('D'), 0x0189, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('D'), 0x018A, DirectionCommandToUnicode}, /** 0x018B */ /** 0x018C */ /** 0x018D */ {QLatin1Char('M'), QLatin1Char('E'), 0x018E, DirectionCommandToUnicode}, /** 0x018F */ {QLatin1Char('m'), QLatin1Char('E'), 0x0190, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('F'), 0x0191, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('f'), 0x0192, DirectionCommandToUnicode}, /** 0x0193 */ {QLatin1Char('m'), QLatin1Char('G'), 0x0194, DirectionCommandToUnicode}, /** 0x0195: see EncoderLaTeXCharacterCommand */ {QLatin1Char('m'), QLatin1Char('I'), 0x0196, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('I'), 0x0197, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('K'), 0x0198, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('k'), 0x0199, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('l'), 0x019A, DirectionCommandToUnicode}, /** 0x019B */ /** 0x019C */ {QLatin1Char('m'), QLatin1Char('J'), 0x019D, DirectionCommandToUnicode}, /** 0x019E */ /** 0x019F */ /** 0x01A0 */ /** 0x01A1 */ /** 0x01A2 */ /** 0x01A3 */ {QLatin1Char('m'), QLatin1Char('P'), 0x01A4, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('p'), 0x01A5, DirectionCommandToUnicode}, /** 0x01A6 */ /** 0x01A7 */ /** 0x01A8 */ /** 0x01A9: see EncoderLaTeXCharacterCommand */ /** 0x01AA */ /** 0x01AB */ {QLatin1Char('m'), QLatin1Char('T'), 0x01AC, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('t'), 0x01AD, DirectionCommandToUnicode}, {QLatin1Char('M'), QLatin1Char('T'), 0x01AE, DirectionCommandToUnicode}, /** 0x01AF */ /** 0x01B0 */ {QLatin1Char('m'), QLatin1Char('U'), 0x01B1, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('V'), 0x01B2, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('Y'), 0x01B3, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('y'), 0x01B4, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('Z'), 0x01B5, DirectionCommandToUnicode}, {QLatin1Char('B'), QLatin1Char('z'), 0x01B6, DirectionCommandToUnicode}, {QLatin1Char('m'), QLatin1Char('Z'), 0x01B7, DirectionCommandToUnicode}, /** 0x01B8 */ /** 0x01B9 */ /** 0x01BA */ {QLatin1Char('B'), QLatin1Char('2'), 0x01BB, DirectionCommandToUnicode}, /** 0x01BC */ /** 0x01BD */ /** 0x01BE */ /** 0x01BF */ /** 0x01C0 */ /** 0x01C1 */ /** 0x01C2 */ /** 0x01C3 */ /** 0x01C4 */ /** 0x01C5 */ /** 0x01C6 */ /** 0x01C7 */ /** 0x01C8 */ /** 0x01C9 */ /** 0x01CA */ /** 0x01CB */ /** 0x01CC */ {QLatin1Char('v'), QLatin1Char('A'), 0x01CD, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('a'), 0x01CE, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('G'), 0x01E6, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('g'), 0x01E7, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('O'), 0x01EA, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('o'), 0x01EB, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('F'), 0x01F4, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('f'), 0x01F5, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('A'), 0x0226, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('a'), 0x0227, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('E'), 0x0228, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('e'), 0x0229, DirectionBoth}, {QLatin1Char('='), QLatin1Char('Y'), 0x0232, DirectionBoth}, {QLatin1Char('='), QLatin1Char('y'), 0x0233, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('O'), 0x022E, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('o'), 0x022F, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('B'), 0x1E02, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('b'), 0x1E03, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('B'), 0x1E04, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('b'), 0x1E05, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('D'), 0x1E0A, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('d'), 0x1E0B, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('D'), 0x1E0C, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('d'), 0x1E0D, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('D'), 0x1E10, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('d'), 0x1E11, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('E'), 0x1E1E, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('e'), 0x1E1F, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('H'), 0x1E22, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('h'), 0x1E23, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('H'), 0x1E24, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('h'), 0x1E25, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('H'), 0x1E26, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('h'), 0x1E27, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('H'), 0x1E28, DirectionBoth}, {QLatin1Char('c'), QLatin1Char('h'), 0x1E29, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('K'), 0x1E32, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('k'), 0x1E33, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('L'), 0x1E36, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('l'), 0x1E37, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('M'), 0x1E40, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('m'), 0x1E41, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('M'), 0x1E42, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('m'), 0x1E43, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('N'), 0x1E44, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('n'), 0x1E45, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('N'), 0x1E46, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('n'), 0x1E47, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('P'), 0x1E56, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('p'), 0x1E57, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('R'), 0x1E58, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('r'), 0x1E59, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('R'), 0x1E5A, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('r'), 0x1E5B, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('S'), 0x1E60, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('s'), 0x1E61, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('S'), 0x1E62, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('s'), 0x1E63, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('T'), 0x1E6A, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('t'), 0x1E6B, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('T'), 0x1E6C, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('t'), 0x1E6D, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('V'), 0x1E7E, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('v'), 0x1E7F, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('W'), 0x1E80, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('w'), 0x1E81, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('W'), 0x1E82, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('w'), 0x1E83, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('W'), 0x1E84, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('w'), 0x1E85, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('W'), 0x1E86, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('w'), 0x1E87, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('W'), 0x1E88, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('w'), 0x1E88, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('X'), 0x1E8A, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('x'), 0x1E8B, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('X'), 0x1E8C, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('x'), 0x1E8D, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('Y'), 0x1E8E, DirectionBoth}, {QLatin1Char('.'), QLatin1Char('y'), 0x1E8F, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('Z'), 0x1E92, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('z'), 0x1E93, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('t'), 0x1E97, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('w'), 0x1E98, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('y'), 0x1E99, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('A'), 0x1EA0, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('a'), 0x1EA1, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('E'), 0x1EB8, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('e'), 0x1EB9, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('I'), 0x1ECA, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('i'), 0x1ECB, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('O'), 0x1ECC, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('o'), 0x1ECD, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('U'), 0x1EE4, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('u'), 0x1EE5, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('Y'), 0x1EF2, DirectionBoth}, {QLatin1Char('`'), QLatin1Char('y'), 0x1EF3, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('Y'), 0x1EF4, DirectionBoth}, {QLatin1Char('d'), QLatin1Char('y'), 0x1EF5, DirectionBoth}, {QLatin1Char('r'), QLatin1Char('q'), 0x2019, DirectionCommandToUnicode} ///< tricky: this is \rq }; /** * This structure contains information on the usage of dotless i * and dotless j in combination with accent-like modifiers. * Combinations such as \"{\i} are translated to an Unicode character * and back. The structure is a table with three columns: (1) the * modified (in the example before the quotation mark) (2) the ASCII * character (in the example before the 'i') (3) the Unicode * character described by a hexcode. */ // TODO other cases of \i and \j? static const struct DotlessIJCharacter { const QChar modifier; const QChar letter; const ushort unicode; const EncoderLaTeXCommandDirection direction; } dotlessIJCharacters[] = { {QLatin1Char('`'), QLatin1Char('i'), 0x00EC, DirectionBoth}, {QLatin1Char('\''), QLatin1Char('i'), 0x00ED, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('i'), 0x00EE, DirectionBoth}, {QLatin1Char('"'), QLatin1Char('i'), 0x00EF, DirectionBoth}, {QLatin1Char('~'), QLatin1Char('i'), 0x0129, DirectionBoth}, {QLatin1Char('='), QLatin1Char('i'), 0x012B, DirectionBoth}, {QLatin1Char('u'), QLatin1Char('i'), 0x012D, DirectionBoth}, {QLatin1Char('k'), QLatin1Char('i'), 0x012F, DirectionBoth}, {QLatin1Char('^'), QLatin1Char('j'), 0x0135, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('i'), 0x01D0, DirectionBoth}, {QLatin1Char('v'), QLatin1Char('j'), 0x01F0, DirectionBoth}, {QLatin1Char('G'), QLatin1Char('i'), 0x0209, DirectionCommandToUnicode} }; /** * This lookup allows to quickly find hits in the * EncoderLaTeXEscapedCharacter table. This data structure here * consists of a number of rows. Each row consists of a * modifier (like '"' or 'v') and an array of Unicode chars. * Letters 'A'..'Z','a'..'z','0'..'9' are used as index to this * array by invocing asciiLetterOrDigitToPos(). * This data structure is built in the constructor. */ static const int lookupTableNumModifiers = 32; static const int lookupTableNumCharacters = 26 * 2 + 10; static struct EncoderLaTeXEscapedCharacterLookupTableRow { QChar modifier; QChar unicode[lookupTableNumCharacters]; } *lookupTable[lookupTableNumModifiers]; /** * This data structure keeps track of math commands, which * have to be treated differently in text and math mode. * The math command like "subset of" could be used directly * in math mode, but must be enclosed in \ensuremath{...} * in text mode. */ static const struct MathCommand { const QString command; const ushort unicode; const EncoderLaTeXCommandDirection direction; } mathCommands[] = { {QStringLiteral("pm"), 0x00B1, DirectionBoth}, {QStringLiteral("times"), 0x00D7, DirectionBoth}, {QStringLiteral("div"), 0x00F7, DirectionBoth}, {QStringLiteral("phi"), 0x0278, DirectionBoth}, ///< see also 0x03C6 (GREEK SMALL LETTER PHI) {QStringLiteral("Alpha"), 0x0391, DirectionBoth}, {QStringLiteral("Beta"), 0x0392, DirectionBoth}, {QStringLiteral("Gamma"), 0x0393, DirectionBoth}, {QStringLiteral("Delta"), 0x0394, DirectionBoth}, {QStringLiteral("Epsilon"), 0x0395, DirectionBoth}, {QStringLiteral("Zeta"), 0x0396, DirectionBoth}, {QStringLiteral("Eta"), 0x0397, DirectionBoth}, {QStringLiteral("Theta"), 0x0398, DirectionBoth}, {QStringLiteral("Iota"), 0x0399, DirectionBoth}, {QStringLiteral("Kappa"), 0x039A, DirectionBoth}, {QStringLiteral("Lamda"), 0x039B, DirectionCommandToUnicode}, ///< \Lamda does not exist, this is mostly for spelling errors {QStringLiteral("Lambda"), 0x039B, DirectionBoth}, {QStringLiteral("Mu"), 0x039C, DirectionBoth}, {QStringLiteral("Nu"), 0x039D, DirectionBoth}, {QStringLiteral("Xi"), 0x039E, DirectionBoth}, {QStringLiteral("Omicron"), 0x039F, DirectionBoth}, {QStringLiteral("Pi"), 0x03A0, DirectionBoth}, {QStringLiteral("Rho"), 0x03A1, DirectionBoth}, {QStringLiteral("Sigma"), 0x03A3, DirectionBoth}, {QStringLiteral("Tau"), 0x03A4, DirectionBoth}, {QStringLiteral("Upsilon"), 0x03A5, DirectionBoth}, {QStringLiteral("Phi"), 0x03A6, DirectionBoth}, {QStringLiteral("Chi"), 0x03A7, DirectionBoth}, {QStringLiteral("Psi"), 0x03A8, DirectionBoth}, {QStringLiteral("Omega"), 0x03A9, DirectionBoth}, {QStringLiteral("alpha"), 0x03B1, DirectionBoth}, {QStringLiteral("beta"), 0x03B2, DirectionBoth}, {QStringLiteral("gamma"), 0x03B3, DirectionBoth}, {QStringLiteral("delta"), 0x03B4, DirectionBoth}, {QStringLiteral("varepsilon"), 0x03B5, DirectionBoth}, {QStringLiteral("zeta"), 0x03B6, DirectionBoth}, {QStringLiteral("eta"), 0x03B7, DirectionBoth}, {QStringLiteral("theta"), 0x03B8, DirectionBoth}, {QStringLiteral("iota"), 0x03B9, DirectionBoth}, {QStringLiteral("kappa"), 0x03BA, DirectionBoth}, {QStringLiteral("lamda"), 0x03BB, DirectionBoth}, ///< \lamda does not exist, this is mostly for spelling errors {QStringLiteral("lambda"), 0x03BB, DirectionBoth}, {QStringLiteral("mu"), 0x03BC, DirectionBoth}, {QStringLiteral("nu"), 0x03BD, DirectionBoth}, {QStringLiteral("xi"), 0x03BE, DirectionBoth}, {QStringLiteral("omicron"), 0x03BF, DirectionBoth}, {QStringLiteral("pi"), 0x03C0, DirectionBoth}, {QStringLiteral("rho"), 0x03C1, DirectionBoth}, {QStringLiteral("varsigma"), 0x03C2, DirectionBoth}, {QStringLiteral("sigma"), 0x03C3, DirectionBoth}, {QStringLiteral("tau"), 0x03C4, DirectionBoth}, {QStringLiteral("upsilon"), 0x03C5, DirectionBoth}, {QStringLiteral("varphi"), 0x03C6, DirectionBoth}, ///< see also 0x0278 (LATIN SMALL LETTER PHI) {QStringLiteral("chi"), 0x03C7, DirectionBoth}, {QStringLiteral("psi"), 0x03C8, DirectionBoth}, {QStringLiteral("omega"), 0x03C9, DirectionBoth}, {QStringLiteral("vartheta"), 0x03D1, DirectionBoth}, {QStringLiteral("varpi"), 0x03D6, DirectionBoth}, {QStringLiteral("digamma"), 0x03DC, DirectionBoth}, {QStringLiteral("varkappa"), 0x03F0, DirectionBoth}, {QStringLiteral("varrho"), 0x03F1, DirectionBoth}, {QStringLiteral("epsilon"), 0x03F5, DirectionBoth}, {QStringLiteral("backepsilon"), 0x03F6, DirectionBoth}, {QStringLiteral("aleph"), 0x05D0, DirectionBoth}, {QStringLiteral("dagger"), 0x2020, DirectionBoth}, {QStringLiteral("ddagger"), 0x2021, DirectionBoth}, {QStringLiteral("mathbb{C}"), 0x2102, DirectionBoth}, {QStringLiteral("ell"), 0x2113, DirectionBoth}, {QStringLiteral("mho"), 0x2127, DirectionBoth}, {QStringLiteral("beth"), 0x2136, DirectionBoth}, {QStringLiteral("gimel"), 0x2137, DirectionBoth}, {QStringLiteral("daleth"), 0x2138, DirectionBoth}, {QStringLiteral("rightarrow"), 0x2192, DirectionBoth}, {QStringLiteral("forall"), 0x2200, DirectionBoth}, {QStringLiteral("complement"), 0x2201, DirectionBoth}, {QStringLiteral("partial"), 0x2202, DirectionBoth}, {QStringLiteral("exists"), 0x2203, DirectionBoth}, {QStringLiteral("nexists"), 0x2204, DirectionBoth}, {QStringLiteral("varnothing"), 0x2205, DirectionBoth}, {QStringLiteral("nabla"), 0x2207, DirectionBoth}, {QStringLiteral("in"), 0x2208, DirectionBoth}, {QStringLiteral("notin"), 0x2209, DirectionBoth}, {QStringLiteral("ni"), 0x220B, DirectionBoth}, {QStringLiteral("not\\ni"), 0x220C, DirectionBoth}, {QStringLiteral("asterisk"), 0x2217, DirectionCommandToUnicode}, {QStringLiteral("infty"), 0x221E, DirectionBoth}, {QStringLiteral("leq"), 0x2264, DirectionBoth}, {QStringLiteral("geq"), 0x2265, DirectionBoth}, {QStringLiteral("lneq"), 0x2268, DirectionBoth}, {QStringLiteral("gneq"), 0x2269, DirectionBoth}, {QStringLiteral("ll"), 0x226A, DirectionBoth}, {QStringLiteral("gg"), 0x226B, DirectionBoth}, {QStringLiteral("nless"), 0x226E, DirectionBoth}, {QStringLiteral("ngtr"), 0x226F, DirectionBoth}, {QStringLiteral("nleq"), 0x2270, DirectionBoth}, {QStringLiteral("ngeq"), 0x2271, DirectionBoth}, {QStringLiteral("subset"), 0x2282, DirectionBoth}, {QStringLiteral("supset"), 0x2283, DirectionBoth}, {QStringLiteral("subseteq"), 0x2286, DirectionBoth}, {QStringLiteral("supseteq"), 0x2287, DirectionBoth}, {QStringLiteral("nsubseteq"), 0x2288, DirectionBoth}, {QStringLiteral("nsupseteq"), 0x2289, DirectionBoth}, {QStringLiteral("subsetneq"), 0x228A, DirectionBoth}, {QStringLiteral("supsetneq"), 0x228A, DirectionBoth}, {QStringLiteral("Subset"), 0x22D0, DirectionBoth}, {QStringLiteral("Supset"), 0x22D1, DirectionBoth}, {QStringLiteral("lll"), 0x22D8, DirectionBoth}, {QStringLiteral("ggg"), 0x22D9, DirectionBoth}, {QStringLiteral("top"), 0x22A4, DirectionBoth}, {QStringLiteral("bot"), 0x22A5, DirectionBoth}, }; /** * This data structure holds commands representing a single * character. For example, it maps \AA to A with a ring (Nordic * letter) and back. The structure is a table with two columns: * (1) the command's name without a backslash (in the example * before the 'AA') (2) the Unicode character described by a * hexcode. */ static const struct EncoderLaTeXCharacterCommand { const QString command; const ushort unicode; const EncoderLaTeXCommandDirection direction; } encoderLaTeXCharacterCommands[] = { {QStringLiteral("textexclamdown"), 0x00A1, DirectionCommandToUnicode}, {QStringLiteral("textcent"), 0x00A2, DirectionBoth}, {QStringLiteral("pounds"), 0x00A3, DirectionBoth}, {QStringLiteral("textsterling"), 0x00A3, DirectionBoth}, /** 0x00A4 */ {QStringLiteral("textyen"), 0x00A5, DirectionBoth}, {QStringLiteral("textbrokenbar"), 0x00A6, DirectionBoth}, {QStringLiteral("S"), 0x00A7, DirectionBoth}, {QStringLiteral("textsection"), 0x00A7, DirectionBoth}, /** 0x00A8 */ {QStringLiteral("copyright"), 0x00A9, DirectionBoth}, {QStringLiteral("textcopyright"), 0x00A9, DirectionBoth}, {QStringLiteral("textordfeminine"), 0x00AA, DirectionBoth}, {QStringLiteral("guillemotleft"), 0x00AB, DirectionCommandToUnicode}, {QStringLiteral("textflqq"), 0x00AB, DirectionCommandToUnicode}, {QStringLiteral("flqq"), 0x00AB, DirectionBoth}, /** 0x00AC */ /** 0x00AD */ {QStringLiteral("textregistered"), 0x00AE, DirectionBoth}, /** 0x00AF */ {QStringLiteral("textdegree"), 0x00B0, DirectionBoth}, {QStringLiteral("textpm"), 0x00B1, DirectionBoth}, {QStringLiteral("textplusminus"), 0x00B1, DirectionCommandToUnicode}, /** 0x00B2 */ /** 0x00B3 */ /** 0x00B4 */ {QStringLiteral("textmu"), 0x00B5, DirectionBoth}, {QStringLiteral("textparagraph"), 0x00B6, DirectionBoth}, {QStringLiteral("textpilcrow"), 0x00B6, DirectionBoth}, {QStringLiteral("textperiodcentered"), 0x00B7, DirectionCommandToUnicode}, {QStringLiteral("textcdot"), 0x00B7, DirectionBoth}, {QStringLiteral("textcentereddot"), 0x00B7, DirectionCommandToUnicode}, /** 0x00B8 */ /** 0x00B9 */ {QStringLiteral("textordmasculine"), 0x00BA, DirectionBoth}, {QStringLiteral("guillemotright"), 0x00BB, DirectionCommandToUnicode}, {QStringLiteral("textfrqq"), 0x00BB, DirectionCommandToUnicode}, {QStringLiteral("frqq"), 0x00BB, DirectionBoth}, {QStringLiteral("textonequarter"), 0x00BC, DirectionBoth}, {QStringLiteral("textonehalf"), 0x00BD, DirectionBoth}, {QStringLiteral("textthreequarters"), 0x00BE, DirectionBoth}, {QStringLiteral("textquestiondown"), 0x00BF, DirectionCommandToUnicode}, // TODO /// recommended to write ?` instead of \textquestiondown {QStringLiteral("AA"), 0x00C5, DirectionBoth}, {QStringLiteral("AE"), 0x00C6, DirectionBoth}, {QStringLiteral("DH"), 0x00D0, DirectionBoth}, {QStringLiteral("texttimes"), 0x00D7, DirectionBoth}, {QStringLiteral("textmultiply"), 0x00D7, DirectionCommandToUnicode}, {QStringLiteral("O"), 0x00D8, DirectionBoth}, {QStringLiteral("TH"), 0x00DE, DirectionBoth}, {QStringLiteral("Thorn"), 0x00DE, DirectionCommandToUnicode}, {QStringLiteral("textThorn"), 0x00DE, DirectionCommandToUnicode}, {QStringLiteral("ss"), 0x00DF, DirectionBoth}, {QStringLiteral("aa"), 0x00E5, DirectionBoth}, {QStringLiteral("ae"), 0x00E6, DirectionBoth}, {QStringLiteral("dh"), 0x00F0, DirectionBoth}, {QStringLiteral("textdiv"), 0x00F7, DirectionBoth}, {QStringLiteral("textdivide"), 0x00F7, DirectionCommandToUnicode}, {QStringLiteral("o"), 0x00F8, DirectionBoth}, {QStringLiteral("th"), 0x00FE, DirectionBoth}, {QStringLiteral("textthorn"), 0x00FE, DirectionCommandToUnicode}, {QStringLiteral("textthornvari"), 0x00FE, DirectionCommandToUnicode}, {QStringLiteral("textthornvarii"), 0x00FE, DirectionCommandToUnicode}, {QStringLiteral("textthornvariii"), 0x00FE, DirectionCommandToUnicode}, {QStringLiteral("textthornvariv"), 0x00FE, DirectionCommandToUnicode}, {QStringLiteral("Aogonek"), 0x0104, DirectionCommandToUnicode}, {QStringLiteral("aogonek"), 0x0105, DirectionCommandToUnicode}, {QStringLiteral("DJ"), 0x0110, DirectionBoth}, {QStringLiteral("dj"), 0x0111, DirectionBoth}, {QStringLiteral("textcrd"), 0x0111, DirectionCommandToUnicode}, {QStringLiteral("textHslash"), 0x0126, DirectionCommandToUnicode}, {QStringLiteral("textHbar"), 0x0126, DirectionCommandToUnicode}, {QStringLiteral("textcrh"), 0x0127, DirectionCommandToUnicode}, {QStringLiteral("texthbar"), 0x0127, DirectionCommandToUnicode}, {QStringLiteral("i"), 0x0131, DirectionBoth}, {QStringLiteral("IJ"), 0x0132, DirectionBoth}, {QStringLiteral("ij"), 0x0133, DirectionBoth}, {QStringLiteral("textkra"), 0x0138, DirectionCommandToUnicode}, {QStringLiteral("Lcaron"), 0x013D, DirectionCommandToUnicode}, {QStringLiteral("lcaron"), 0x013E, DirectionCommandToUnicode}, {QStringLiteral("L"), 0x0141, DirectionBoth}, {QStringLiteral("Lstroke"), 0x0141, DirectionCommandToUnicode}, {QStringLiteral("l"), 0x0142, DirectionBoth}, {QStringLiteral("lstroke"), 0x0142, DirectionCommandToUnicode}, {QStringLiteral("textbarl"), 0x0142, DirectionCommandToUnicode}, {QStringLiteral("NG"), 0x014A, DirectionBoth}, {QStringLiteral("ng"), 0x014B, DirectionBoth}, {QStringLiteral("OE"), 0x0152, DirectionBoth}, {QStringLiteral("oe"), 0x0153, DirectionBoth}, {QStringLiteral("Racute"), 0x0154, DirectionCommandToUnicode}, {QStringLiteral("racute"), 0x0155, DirectionCommandToUnicode}, {QStringLiteral("Sacute"), 0x015A, DirectionCommandToUnicode}, {QStringLiteral("sacute"), 0x015B, DirectionCommandToUnicode}, {QStringLiteral("Scedilla"), 0x015E, DirectionCommandToUnicode}, {QStringLiteral("scedilla"), 0x015F, DirectionCommandToUnicode}, {QStringLiteral("Scaron"), 0x0160, DirectionCommandToUnicode}, {QStringLiteral("scaron"), 0x0161, DirectionCommandToUnicode}, {QStringLiteral("Tcaron"), 0x0164, DirectionCommandToUnicode}, {QStringLiteral("tcaron"), 0x0165, DirectionCommandToUnicode}, {QStringLiteral("textTstroke"), 0x0166, DirectionCommandToUnicode}, {QStringLiteral("textTbar"), 0x0166, DirectionCommandToUnicode}, {QStringLiteral("textTslash"), 0x0166, DirectionCommandToUnicode}, {QStringLiteral("texttstroke"), 0x0167, DirectionCommandToUnicode}, {QStringLiteral("texttbar"), 0x0167, DirectionCommandToUnicode}, {QStringLiteral("texttslash"), 0x0167, DirectionCommandToUnicode}, {QStringLiteral("Zdotaccent"), 0x017B, DirectionCommandToUnicode}, {QStringLiteral("zdotaccent"), 0x017C, DirectionCommandToUnicode}, {QStringLiteral("Zcaron"), 0x017D, DirectionCommandToUnicode}, {QStringLiteral("zcaron"), 0x017E, DirectionCommandToUnicode}, {QStringLiteral("textlongs"), 0x017F, DirectionCommandToUnicode}, {QStringLiteral("textcrb"), 0x0180, DirectionCommandToUnicode}, {QStringLiteral("textBhook"), 0x0181, DirectionCommandToUnicode}, {QStringLiteral("texthausaB"), 0x0181, DirectionCommandToUnicode}, {QStringLiteral("textOopen"), 0x0186, DirectionCommandToUnicode}, {QStringLiteral("textChook"), 0x0187, DirectionCommandToUnicode}, {QStringLiteral("textchook"), 0x0188, DirectionCommandToUnicode}, {QStringLiteral("texthtc"), 0x0188, DirectionCommandToUnicode}, {QStringLiteral("textDafrican"), 0x0189, DirectionCommandToUnicode}, {QStringLiteral("textDhook"), 0x018A, DirectionCommandToUnicode}, {QStringLiteral("texthausaD"), 0x018A, DirectionCommandToUnicode}, {QStringLiteral("textEreversed"), 0x018E, DirectionCommandToUnicode}, {QStringLiteral("textrevE"), 0x018E, DirectionCommandToUnicode}, {QStringLiteral("textEopen"), 0x0190, DirectionCommandToUnicode}, {QStringLiteral("textFhook"), 0x0191, DirectionCommandToUnicode}, {QStringLiteral("textflorin"), 0x0192, DirectionBoth}, {QStringLiteral("textgamma"), 0x0194, DirectionCommandToUnicode}, {QStringLiteral("textGammaafrican"), 0x0194, DirectionCommandToUnicode}, {QStringLiteral("hv"), 0x0195, DirectionCommandToUnicode}, {QStringLiteral("texthvlig"), 0x0195, DirectionCommandToUnicode}, {QStringLiteral("textIotaafrican"), 0x0196, DirectionCommandToUnicode}, {QStringLiteral("textKhook"), 0x0198, DirectionCommandToUnicode}, {QStringLiteral("texthausaK"), 0x0198, DirectionCommandToUnicode}, {QStringLiteral("texthtk"), 0x0199, DirectionCommandToUnicode}, {QStringLiteral("textkhook"), 0x0199, DirectionCommandToUnicode}, {QStringLiteral("textbarl"), 0x019A, DirectionCommandToUnicode}, {QStringLiteral("textcrlambda"), 0x019B, DirectionCommandToUnicode}, {QStringLiteral("textNhookleft"), 0x019D, DirectionCommandToUnicode}, {QStringLiteral("textnrleg"), 0x019E, DirectionCommandToUnicode}, {QStringLiteral("textPUnrleg"), 0x019E, DirectionCommandToUnicode}, {QStringLiteral("Ohorn"), 0x01A0, DirectionCommandToUnicode}, {QStringLiteral("ohorn"), 0x01A1, DirectionCommandToUnicode}, {QStringLiteral("textPhook"), 0x01A4, DirectionCommandToUnicode}, {QStringLiteral("texthtp"), 0x01A5, DirectionCommandToUnicode}, {QStringLiteral("textphook"), 0x01A5, DirectionCommandToUnicode}, {QStringLiteral("ESH"), 0x01A9, DirectionCommandToUnicode}, {QStringLiteral("textEsh"), 0x01A9, DirectionCommandToUnicode}, {QStringLiteral("textlooptoprevsh"), 0x01AA, DirectionCommandToUnicode}, {QStringLiteral("textlhtlongi"), 0x01AA, DirectionCommandToUnicode}, {QStringLiteral("textlhookt"), 0x01AB, DirectionCommandToUnicode}, {QStringLiteral("textThook"), 0x01AC, DirectionCommandToUnicode}, {QStringLiteral("textthook"), 0x01AD, DirectionCommandToUnicode}, {QStringLiteral("texthtt"), 0x01AD, DirectionCommandToUnicode}, {QStringLiteral("textTretroflexhook"), 0x01AE, DirectionCommandToUnicode}, {QStringLiteral("Uhorn"), 0x01AF, DirectionCommandToUnicode}, {QStringLiteral("uhorn"), 0x01B0, DirectionCommandToUnicode}, {QStringLiteral("textupsilon"), 0x01B1, DirectionCommandToUnicode}, {QStringLiteral("textVhook"), 0x01B2, DirectionCommandToUnicode}, {QStringLiteral("textYhook"), 0x01B3, DirectionCommandToUnicode}, {QStringLiteral("textvhook"), 0x01B4, DirectionCommandToUnicode}, {QStringLiteral("Zbar"), 0x01B5, DirectionCommandToUnicode}, {QStringLiteral("zbar"), 0x01B6, DirectionCommandToUnicode}, {QStringLiteral("EZH"), 0x01B7, DirectionCommandToUnicode}, {QStringLiteral("textEzh"), 0x01B7, DirectionCommandToUnicode}, {QStringLiteral("LJ"), 0x01C7, DirectionCommandToUnicode}, {QStringLiteral("Lj"), 0x01C8, DirectionCommandToUnicode}, {QStringLiteral("lj"), 0x01C9, DirectionCommandToUnicode}, {QStringLiteral("NJ"), 0x01CA, DirectionCommandToUnicode}, {QStringLiteral("Nj"), 0x01CB, DirectionCommandToUnicode}, {QStringLiteral("nj"), 0x01CC, DirectionCommandToUnicode}, {QStringLiteral("DZ"), 0x01F1, DirectionCommandToUnicode}, {QStringLiteral("Dz"), 0x01F2, DirectionCommandToUnicode}, {QStringLiteral("dz"), 0x01F3, DirectionCommandToUnicode}, {QStringLiteral("HV"), 0x01F6, DirectionCommandToUnicode}, {QStringLiteral("j"), 0x0237, DirectionBoth}, {QStringLiteral("ldots"), 0x2026, DirectionBoth}, {QStringLiteral("grqq"), 0x201C, DirectionCommandToUnicode}, {QStringLiteral("textquotedblleft"), 0x201C, DirectionCommandToUnicode}, {QStringLiteral("rqq"), 0x201D, DirectionCommandToUnicode}, {QStringLiteral("textquotedblright"), 0x201D, DirectionCommandToUnicode}, {QStringLiteral("glqq"), 0x201E, DirectionCommandToUnicode}, {QStringLiteral("SS"), 0x1E9E, DirectionBoth}, {QStringLiteral("textendash"), 0x2013, DirectionCommandToUnicode}, {QStringLiteral("textemdash"), 0x2014, DirectionCommandToUnicode}, {QStringLiteral("textquoteleft"), 0x2018, DirectionCommandToUnicode}, {QStringLiteral("lq"), 0x2018, DirectionBoth}, {QStringLiteral("textquoteright"), 0x2019, DirectionCommandToUnicode}, {QStringLiteral("rq"), 0x2019, DirectionBoth}, ///< tricky one: 'r' is a valid modifier {QStringLiteral("quotesinglbase"), 0x201A, DirectionBoth}, {QStringLiteral("quotedblbase"), 0x201E, DirectionBoth}, {QStringLiteral("textbullet "), 0x2022, DirectionBoth}, {QStringLiteral("guilsinglleft "), 0x2039, DirectionBoth}, {QStringLiteral("guilsinglright "), 0x203A, DirectionBoth}, {QStringLiteral("textcelsius"), 0x2103, DirectionBoth}, {QStringLiteral("textleftarrow"), 0x2190, DirectionBoth}, {QStringLiteral("textuparrow"), 0x2191, DirectionBoth}, {QStringLiteral("textrightarrow"), 0x2192, DirectionBoth}, {QStringLiteral("textdownarrow"), 0x2193, DirectionBoth} }; const QChar EncoderLaTeX::encoderLaTeXProtectedSymbols[] = {QLatin1Char('#'), QLatin1Char('&'), QLatin1Char('%')}; const QChar EncoderLaTeX::encoderLaTeXProtectedTextOnlySymbols[] = {QLatin1Char('_')}; /** * This data structure holds LaTeX symbol sequences (without * any backslash) that represent a single Unicode character. * For example, it maps --- to an 'em dash' and back. * The structure is a table with two columns: (1) the symbol * sequence (in the example before the '---') (2) the Unicode * character described by a hexcode. */ static const struct EncoderLaTeXSymbolSequence { const QString latex; const ushort unicode; const EncoderLaTeXCommandDirection direction; } encoderLaTeXSymbolSequences[] = { {QStringLiteral("!`"), 0x00A1, DirectionBoth}, {QStringLiteral("\"<"), 0x00AB, DirectionBoth}, {QStringLiteral("\">"), 0x00BB, DirectionBoth}, {QStringLiteral("?`"), 0x00BF, DirectionBoth}, {QStringLiteral("---"), 0x2014, DirectionBoth}, ///< --- must come before -- {QStringLiteral("--"), 0x2013, DirectionBoth}, {QStringLiteral("``"), 0x201C, DirectionBoth}, {QStringLiteral("''"), 0x201D, DirectionBoth}, {QStringLiteral("ff"), 0xFB00, DirectionUnicodeToCommand}, {QStringLiteral("fi"), 0xFB01, DirectionUnicodeToCommand}, {QStringLiteral("fl"), 0xFB02, DirectionUnicodeToCommand}, {QStringLiteral("ffi"), 0xFB03, DirectionUnicodeToCommand}, {QStringLiteral("ffl"), 0xFB04, DirectionUnicodeToCommand}, {QStringLiteral("ft"), 0xFB05, DirectionUnicodeToCommand}, {QStringLiteral("st"), 0xFB06, DirectionUnicodeToCommand} }; EncoderLaTeX::EncoderLaTeX() : Encoder() { /// Initialize lookup table with NULL pointers for (int i = 0; i < lookupTableNumModifiers; ++i) lookupTable[i] = nullptr; int lookupTableCount = 0; /// Go through all table rows of encoderLaTeXEscapedCharacters for (const EncoderLaTeXEscapedCharacter &encoderLaTeXEscapedCharacter : encoderLaTeXEscapedCharacters) { /// Check if this row's modifier is already known bool knownModifier = false; int j; for (j = lookupTableCount - 1; j >= 0; --j) { knownModifier |= lookupTable[j]->modifier == encoderLaTeXEscapedCharacter.modifier; if (knownModifier) break; } if (!knownModifier) { /// Ok, this row's modifier appeared for the first time, /// therefore initialize memory structure, i.e. row in lookupTable lookupTable[lookupTableCount] = new EncoderLaTeXEscapedCharacterLookupTableRow; lookupTable[lookupTableCount]->modifier = encoderLaTeXEscapedCharacter.modifier; /// If no special character is known for a letter+modifier /// combination, fall back using the ASCII character only for (ushort k = 0; k < 26; ++k) { lookupTable[lookupTableCount]->unicode[k] = QChar(QLatin1Char('A').unicode() + k); lookupTable[lookupTableCount]->unicode[k + 26] = QChar(QLatin1Char('a').unicode() + k); } for (ushort k = 0; k < 10; ++k) lookupTable[lookupTableCount]->unicode[k + 52] = QChar(QLatin1Char('0').unicode() + k); j = lookupTableCount; ++lookupTableCount; } /// Add the letter as of the current row in encoderLaTeXEscapedCharacters /// into Unicode char array in the current modifier's row in the lookup table. int pos = -1; if ((pos = asciiLetterOrDigitToPos(encoderLaTeXEscapedCharacter.letter)) >= 0) lookupTable[j]->unicode[pos] = QChar(encoderLaTeXEscapedCharacter.unicode); else qCWarning(LOG_KBIBTEX_IO) << "Cannot handle letter " << encoderLaTeXEscapedCharacter.letter; } } EncoderLaTeX::~EncoderLaTeX() { /// Clean-up memory for (int i = lookupTableNumModifiers - 1; i >= 0; --i) if (lookupTable[i] != nullptr) delete lookupTable[i]; } QString EncoderLaTeX::decode(const QString &input) const { const int len = input.length(); QString output; output.reserve(len); bool inMathMode = false; int cachedAsciiLetterOrDigitToPos = -1; /// Go through input char by char for (int i = 0; i < len; ++i) { /** * Repeatedly check if input data contains a verbatim command * like \url{...}, copy it to output, and update i to point * to the next character after the verbatim command. */ while (testAndCopyVerbatimCommands(input, i, output)); if (i >= len) break; /// Fetch current input char const QChar c = input[i]; if (c == QLatin1Char('{')) { /// First case: An opening curly bracket, /// which is harmless (see else case), unless ... if (i < len - 3 && input[i + 1] == QLatin1Char('\\')) { /// ... it continues with a backslash /// Next, check if there follows a modifier after the backslash /// For example an quotation mark as used in {\"a} const int lookupTablePos = modifierInLookupTable(input[i + 2].toLatin1()); /// Check for spaces between modifier and character, for example /// like {\H o} int skipSpaces = 0; while (i + 3 + skipSpaces < len && input[i + 3 + skipSpaces] == QLatin1Char(' ') && skipSpaces < 16) ++skipSpaces; if (lookupTablePos >= 0 && i + skipSpaces < len - 4 && (cachedAsciiLetterOrDigitToPos = asciiLetterOrDigitToPos(input[i + 3 + skipSpaces])) >= 0 && input[i + 4 + skipSpaces] == QLatin1Char('}')) { /// If we found a modifier which is followed by /// a letter followed by a closing curly bracket, /// we are looking at something like {\"A} /// Use lookup table to see what Unicode char this /// represents const QChar unicodeLetter = lookupTable[lookupTablePos]->unicode[cachedAsciiLetterOrDigitToPos]; if (unicodeLetter.unicode() < 127) { /// This combination of modifier and letter is not known, /// so try to preserve it output.append(input.midRef(i, 5 + skipSpaces)); qCWarning(LOG_KBIBTEX_IO) << "Don't know how to translate this into Unicode: " << input.mid(i, 5 + skipSpaces); } else output.append(unicodeLetter); /// Step over those additional characters i += 4 + skipSpaces; } else if (lookupTablePos >= 0 && i + skipSpaces < len - 5 && input[i + 3 + skipSpaces] == QLatin1Char('\\') && isIJ(input[i + 4 + skipSpaces]) && input[i + 5 + skipSpaces] == QLatin1Char('}')) { /// This is the case for {\'\i} or alike. bool found = false; for (const DotlessIJCharacter &dotlessIJCharacter : dotlessIJCharacters) if (dotlessIJCharacter.letter == input[i + 4 + skipSpaces] && dotlessIJCharacter.modifier == input[i + 2]) { output.append(QChar(dotlessIJCharacter.unicode)); i += 5 + skipSpaces; found = true; break; } if (!found) qCWarning(LOG_KBIBTEX_IO) << "Cannot interpret BACKSLASH" << input[i + 2] << "BACKSLASH" << input[i + 4 + skipSpaces]; } else if (lookupTablePos >= 0 && i + skipSpaces < len - 6 && input[i + 3 + skipSpaces] == QLatin1Char('{') && (cachedAsciiLetterOrDigitToPos = asciiLetterOrDigitToPos(input[i + 4 + skipSpaces])) >= 0 && input[i + 5 + skipSpaces] == QLatin1Char('}') && input[i + 6 + skipSpaces] == QLatin1Char('}')) { /// If we found a modifier which is followed by /// an opening curly bracket followed by a letter /// followed by two closing curly brackets, /// we are looking at something like {\"{A}} /// Use lookup table to see what Unicode char this /// represents const QChar unicodeLetter = lookupTable[lookupTablePos]->unicode[cachedAsciiLetterOrDigitToPos]; if (unicodeLetter.unicode() < 127) { /// This combination of modifier and letter is not known, /// so try to preserve it output.append(input.midRef(i, 7 + skipSpaces)); qCWarning(LOG_KBIBTEX_IO) << "Don't know how to translate this into Unicode: " << input.mid(i, 7 + skipSpaces); } else output.append(unicodeLetter); /// Step over those additional characters i += 6 + skipSpaces; } else if (lookupTablePos >= 0 && i + skipSpaces < len - 7 && input[i + 3 + skipSpaces] == QLatin1Char('{') && input[i + 4 + skipSpaces] == QLatin1Char('\\') && isIJ(input[i + 5 + skipSpaces]) && input[i + 6 + skipSpaces] == QLatin1Char('}') && input[i + 7 + skipSpaces] == QLatin1Char('}')) { /// This is the case for {\'{\i}} or alike. bool found = false; for (const DotlessIJCharacter &dotlessIJCharacter : dotlessIJCharacters) if (dotlessIJCharacter.letter == input[i + 5 + skipSpaces] && dotlessIJCharacter.modifier == input[i + 2]) { output.append(QChar(dotlessIJCharacter.unicode)); i += 7 + skipSpaces; found = true; break; } if (!found) qCWarning(LOG_KBIBTEX_IO) << "Cannot interpret BACKSLASH" << input[i + 2] << "BACKSLASH {" << input[i + 5 + skipSpaces] << "}"; } else { /// Now, the case of something like {\AA} is left /// to check for const QString alpha = readAlphaCharacters(input, i + 2); int nextPosAfterAlpha = i + 2 + alpha.size(); if (nextPosAfterAlpha < input.length() && input[nextPosAfterAlpha] == QLatin1Char('}')) { /// We are dealing actually with a string like {\AA} /// Check which command it is, /// insert corresponding Unicode character bool foundCommand = false; for (const EncoderLaTeXCharacterCommand &encoderLaTeXCharacterCommand : encoderLaTeXCharacterCommands) { if (encoderLaTeXCharacterCommand.command == alpha) { output.append(QChar(encoderLaTeXCharacterCommand.unicode)); foundCommand = true; break; } } /// Check if a math command has been read, /// like \subset /// (automatically skipped if command was found above) for (const MathCommand &mathCommand : mathCommands) { if (mathCommand.command == alpha) { if (output.endsWith(QStringLiteral("\\ensuremath"))) { /// Remove "\ensuremath" right before this math command, /// it will be re-inserted when exporting/saving the document output = output.left(output.length() - 11); } output.append(QChar(mathCommand.unicode)); foundCommand = true; break; } } if (foundCommand) i = nextPosAfterAlpha; else { /// Dealing with a string line {\noopsort} /// (see BibTeX documentation where this gets explained) output.append(c); } } else { /// Could be something like {\tt filename.txt} /// Keep it as it is output.append(c); } } } else { /// Nothing special, copy input char to output output.append(c); } } else if (c == QLatin1Char('\\') && i < len - 1) { /// Second case: A backslash as in \"o /// Sometimes such command are closed with just {}, /// so remember if to check for that bool checkForExtraCurlyAtEnd = false; /// Check if there follows a modifier after the backslash /// For example an quotation mark as used in \"a const int lookupTablePos = modifierInLookupTable(input[i + 1]); /// Check for spaces between modifier and character, for example /// like \H o int skipSpaces = 0; while (i + 2 + skipSpaces < len && input[i + 2 + skipSpaces] == QLatin1Char(' ') && skipSpaces < 16) ++skipSpaces; if (lookupTablePos >= 0 && i + skipSpaces <= len - 3 && (cachedAsciiLetterOrDigitToPos = asciiLetterOrDigitToPos(input[i + 2 + skipSpaces])) >= 0 && (i + skipSpaces == len - 3 || input[i + 1] == QLatin1Char('"') || input[i + 1] == QLatin1Char('\'') || input[i + 1] == QLatin1Char('`') || input[i + 1] == QLatin1Char('='))) { // TODO more special cases? /// We found a special modifier which is followed by /// a letter followed by normal text without any /// delimiter, so we are looking at something like /// \"u inside Kr\"uger /// Use lookup table to see what Unicode char this /// represents const QChar unicodeLetter = lookupTable[lookupTablePos]->unicode[cachedAsciiLetterOrDigitToPos]; if (unicodeLetter.unicode() < 127) { /// This combination of modifier and letter is not known, /// so try to preserve it output.append(input.midRef(i, 3 + skipSpaces)); qCWarning(LOG_KBIBTEX_IO) << "Don't know how to translate this into Unicode: " << input.mid(i, 3 + skipSpaces); } else output.append(unicodeLetter); /// Step over those additional characters i += 2 + skipSpaces; } else if (lookupTablePos >= 0 && i + skipSpaces <= len - 3 && i + skipSpaces <= len - 3 && (cachedAsciiLetterOrDigitToPos = asciiLetterOrDigitToPos(input[i + 2 + skipSpaces])) >= 0 && (i + skipSpaces == len - 3 || input[i + 3 + skipSpaces] == QLatin1Char('}') || input[i + 3 + skipSpaces] == QLatin1Char('{') || input[i + 3 + skipSpaces] == QLatin1Char(' ') || input[i + 3 + skipSpaces] == QLatin1Char('\t') || input[i + 3 + skipSpaces] == QLatin1Char('\\') || input[i + 3 + skipSpaces] == QLatin1Char('\r') || input[i + 3 + skipSpaces] == QLatin1Char('\n'))) { /// We found a modifier which is followed by /// a letter followed by a command delimiter such /// as a whitespace, so we are looking at something /// like \"u followed by a space /// Use lookup table to see what Unicode char this /// represents const QChar unicodeLetter = lookupTable[lookupTablePos]->unicode[cachedAsciiLetterOrDigitToPos]; if (unicodeLetter.unicode() < 127) { /// This combination of modifier and letter is not known, /// so try to preserve it output.append(input.midRef(i, 3)); qCWarning(LOG_KBIBTEX_IO) << "Don't know how to translate this into Unicode: " << input.mid(i, 3); } else output.append(unicodeLetter); /// Step over those additional characters i += 2 + skipSpaces; /// Now, after this command, a whitespace may follow /// which has to get "eaten" as it acts as a command /// delimiter if (input[i + 1] == QLatin1Char(' ') || input[i + 1] == QLatin1Char('\r') || input[i + 1] == QLatin1Char('\n')) ++i; else { /// If no whitespace follows, still /// check for extra curly brackets checkForExtraCurlyAtEnd = true; } } else if (lookupTablePos >= 0 && i + skipSpaces < len - 4 && input[i + 2 + skipSpaces] == QLatin1Char('{') && (cachedAsciiLetterOrDigitToPos = asciiLetterOrDigitToPos(input[i + 3 + skipSpaces])) >= 0 && input[i + 4 + skipSpaces] == QLatin1Char('}')) { /// We found a modifier which is followed by an opening /// curly bracket followed a letter followed by a closing /// curly bracket, so we are looking at something /// like \"{u} /// Use lookup table to see what Unicode char this /// represents const QChar unicodeLetter = lookupTable[lookupTablePos]->unicode[cachedAsciiLetterOrDigitToPos]; if (unicodeLetter.unicode() < 127) { /// This combination of modifier and letter is not known, /// so try to preserve it output.append(input.midRef(i, 5 + skipSpaces)); qCWarning(LOG_KBIBTEX_IO) << "Don't know how to translate this into Unicode: " << input.mid(i, 5 + skipSpaces); } else output.append(unicodeLetter); /// Step over those additional characters i += 4 + skipSpaces; } else if (lookupTablePos >= 0 && i + skipSpaces < len - 3 && input[i + 2 + skipSpaces] == QLatin1Char('\\') && isIJ(input[i + 3 + skipSpaces])) { /// This is the case for \'\i or alike. bool found = false; for (const DotlessIJCharacter &dotlessIJCharacter : dotlessIJCharacters) if (dotlessIJCharacter.letter == input[i + 3 + skipSpaces] && dotlessIJCharacter.modifier == input[i + 1]) { output.append(QChar(dotlessIJCharacter.unicode)); i += 3 + skipSpaces; found = true; break; } if (!found) qCWarning(LOG_KBIBTEX_IO) << "Cannot interpret BACKSLASH" << input[i + 1] << "BACKSLASH" << input[i + 3 + skipSpaces]; } else if (lookupTablePos >= 0 && i + skipSpaces < len - 5 && input[i + 2 + skipSpaces] == QLatin1Char('{') && input[i + 3 + skipSpaces] == QLatin1Char('\\') && isIJ(input[i + 4 + skipSpaces]) && input[i + 5 + skipSpaces] == QLatin1Char('}')) { /// This is the case for \'{\i} or alike. bool found = false; for (const DotlessIJCharacter &dotlessIJCharacter : dotlessIJCharacters) if (dotlessIJCharacter.letter == input[i + 4 + skipSpaces] && dotlessIJCharacter.modifier == input[i + 1]) { output.append(QChar(dotlessIJCharacter.unicode)); i += 5 + skipSpaces; found = true; break; } if (!found) qCWarning(LOG_KBIBTEX_IO) << "Cannot interpret BACKSLASH" << input[i + 1] << "BACKSLASH {" << input[i + 4 + skipSpaces] << "}"; } else if (i < len - 1) { /// Now, the case of something like \AA is left /// to check for const QString alpha = readAlphaCharacters(input, i + 1); int nextPosAfterAlpha = i + 1 + alpha.size(); if (alpha.size() >= 1 && alpha.at(0).isLetter()) { /// We are dealing actually with a string like \AA or \o /// Check which command it is, /// insert corresponding Unicode character bool foundCommand = false; for (const EncoderLaTeXCharacterCommand &encoderLaTeXCharacterCommand : encoderLaTeXCharacterCommands) { if (encoderLaTeXCharacterCommand.command == alpha) { output.append(QChar(encoderLaTeXCharacterCommand.unicode)); foundCommand = true; break; } } if (foundCommand) { /// Now, after a command, a whitespace may follow /// which has to get "eaten" as it acts as a command /// delimiter if (nextPosAfterAlpha < input.length() && (input[nextPosAfterAlpha] == QLatin1Char(' ') || input[nextPosAfterAlpha] == QLatin1Char('\r') || input[nextPosAfterAlpha] == QLatin1Char('\n'))) ++nextPosAfterAlpha; else { /// If no whitespace follows, still /// check for extra curly brackets checkForExtraCurlyAtEnd = true; } i = nextPosAfterAlpha - 1; } else { /// No command found? Just copy input char to output output.append(c); } } else { /// Maybe we are dealing with a string like \& or \_ /// Check which command it is bool foundCommand = false; for (const QChar &encoderLaTeXProtectedSymbol : encoderLaTeXProtectedSymbols) if (encoderLaTeXProtectedSymbol == input[i + 1]) { output.append(encoderLaTeXProtectedSymbol); foundCommand = true; break; } if (!foundCommand && !inMathMode) for (const QChar &encoderLaTeXProtectedTextOnlySymbol : encoderLaTeXProtectedTextOnlySymbols) if (encoderLaTeXProtectedTextOnlySymbol == input[i + 1]) { output.append(encoderLaTeXProtectedTextOnlySymbol); foundCommand = true; break; } /// If command has been found, nothing has to be done /// except for hopping over this backslash if (foundCommand) ++i; else if (i < len - 1 && input[i + 1] == QChar(0x002c /* comma */)) { /// Found a thin space: \, /// Replacing Latex-like thin space with Unicode thin space output.append(QChar(0x2009)); // foundCommand = true; ///< only necessary if more tests will follow in the future ++i; } else { /// Nothing special, copy input char to output output.append(c); } } } else { /// Nothing special, copy input char to output output.append(c); } /// Finally, check if there may be extra curly brackets /// like {} and hop over them if (checkForExtraCurlyAtEnd && i < len - 2 && input[i + 1] == QLatin1Char('{') && input[i + 2] == QLatin1Char('}')) i += 2; } else { /// So far, no opening curly bracket and no backslash /// May still be a symbol sequence like --- bool isSymbolSequence = false; /// Go through all known symbol sequnces for (const EncoderLaTeXSymbolSequence &encoderLaTeXSymbolSequence : encoderLaTeXSymbolSequences) { /// First, check if read input character matches beginning of symbol sequence /// and input buffer as enough characters left to potentially contain /// symbol sequence const int latexLen = encoderLaTeXSymbolSequence.latex.length(); if ((encoderLaTeXSymbolSequence.direction & DirectionCommandToUnicode) && encoderLaTeXSymbolSequence.latex[0] == c && i <= len - latexLen) { /// Now actually check if symbol sequence is in input buffer isSymbolSequence = true; for (int p = 1; isSymbolSequence && p < latexLen; ++p) isSymbolSequence &= encoderLaTeXSymbolSequence.latex[p] == input[i + p]; if (isSymbolSequence) { /// Ok, found sequence: insert Unicode character in output /// and hop over sequence in input buffer output.append(QChar(encoderLaTeXSymbolSequence.unicode)); i += encoderLaTeXSymbolSequence.latex.length() - 1; break; } } } if (!isSymbolSequence) { /// No symbol sequence found, so just copy input to output output.append(c); /// Still, check if input character is a dollar sign /// without a preceding backslash, means toggling between /// text mode and math mode if (c == QLatin1Char('$') && (i == 0 || input[i - 1] != QLatin1Char('\\'))) inMathMode = !inMathMode; } } } output.squeeze(); return output; } bool EncoderLaTeX::testAndCopyVerbatimCommands(const QString &input, int &pos, QString &output) const { int copyBytesCount = 0; int openedClosedCurlyBrackets = 0; /// check for \url if (pos < input.length() - 6 && input.mid(pos, 5) == QStringLiteral("\\url{")) { copyBytesCount = 5; openedClosedCurlyBrackets = 1; } if (copyBytesCount > 0) { while (openedClosedCurlyBrackets > 0 && pos + copyBytesCount < input.length()) { ++copyBytesCount; if (input[pos + copyBytesCount] == QLatin1Char('{') && input[pos + copyBytesCount - 1] != QLatin1Char('\\')) ++openedClosedCurlyBrackets; else if (input[pos + copyBytesCount] == QLatin1Char('}') && input[pos + copyBytesCount - 1] != QLatin1Char('\\')) --openedClosedCurlyBrackets; } output.append(input.midRef(pos, copyBytesCount)); pos += copyBytesCount; } return copyBytesCount > 0; } QString EncoderLaTeX::encode(const QString &ninput, const TargetEncoding targetEncoding) const { /// Perform Canonical Decomposition followed by Canonical Composition const QString input = ninput.normalized(QString::NormalizationForm_C); int len = input.length(); QString output; output.reserve(len); bool inMathMode = false; /// Go through input char by char for (int i = 0; i < len; ++i) { /** * Repeatedly check if input data contains a verbatim command * like \url{...}, append it to output, and update i to point * to the next character after the verbatim command. */ while (testAndCopyVerbatimCommands(input, i, output)); if (i >= len) break; const QChar c = input[i]; - if (targetEncoding == TargetEncodingASCII && c.unicode() > 127) { + if (targetEncoding == TargetEncoding::ASCII && c.unicode() > 127) { /// If current char is outside ASCII boundaries ... bool found = false; /// Handle special cases of i without a dot (\i) for (const DotlessIJCharacter &dotlessIJCharacter : dotlessIJCharacters) if (c.unicode() == dotlessIJCharacter.unicode && (dotlessIJCharacter.direction & DirectionUnicodeToCommand)) { output.append(QString(QStringLiteral("{\\%1\\%2}")).arg(dotlessIJCharacter.modifier, dotlessIJCharacter.letter)); found = true; break; } if (!found) { /// ... test if there is a symbol sequence like --- /// to encode it for (const EncoderLaTeXSymbolSequence &encoderLaTeXSymbolSequence : encoderLaTeXSymbolSequences) if (encoderLaTeXSymbolSequence.unicode == c.unicode() && (encoderLaTeXSymbolSequence.direction & DirectionUnicodeToCommand)) { for (int l = 0; l < encoderLaTeXSymbolSequence.latex.length(); ++l) output.append(encoderLaTeXSymbolSequence.latex[l]); found = true; break; } } if (!found) { /// Ok, no symbol sequence. Let's test character /// commands like \ss for (const EncoderLaTeXCharacterCommand &encoderLaTeXCharacterCommand : encoderLaTeXCharacterCommands) if (encoderLaTeXCharacterCommand.unicode == c.unicode() && (encoderLaTeXCharacterCommand.direction & DirectionUnicodeToCommand)) { output.append(QString(QStringLiteral("{\\%1}")).arg(encoderLaTeXCharacterCommand.command)); found = true; break; } } if (!found) { /// Ok, neither a character command. Let's test /// escaped characters with modifiers like \"a for (const EncoderLaTeXEscapedCharacter &encoderLaTeXEscapedCharacter : encoderLaTeXEscapedCharacters) if (encoderLaTeXEscapedCharacter.unicode == c.unicode() && (encoderLaTeXEscapedCharacter.direction & DirectionUnicodeToCommand)) { const QString formatString = isAsciiLetter(encoderLaTeXEscapedCharacter.modifier) ? QStringLiteral("{\\%1 %2}") : QStringLiteral("{\\%1%2}"); output.append(formatString.arg(encoderLaTeXEscapedCharacter.modifier).arg(encoderLaTeXEscapedCharacter.letter)); found = true; break; } } if (!found) { /// Ok, test for math commands for (const MathCommand &mathCommand : mathCommands) if (mathCommand.unicode == c.unicode() && (mathCommand.direction & DirectionUnicodeToCommand)) { if (inMathMode) output.append(QString(QStringLiteral("\\%1{}")).arg(mathCommand.command)); else output.append(QString(QStringLiteral("\\ensuremath{\\%1}")).arg(mathCommand.command)); found = true; break; } } if (!found && c.unicode() == 0x2009) { /// Thin space output.append(QStringLiteral("\\,")); found = true; } if (!found) { qCWarning(LOG_KBIBTEX_IO) << "Don't know how to encode Unicode char" << QString(QStringLiteral("0x%1")).arg(c.unicode(), 4, 16, QLatin1Char('0')); output.append(c); } } else { /// Current character is normal ASCII /// and targetEncoding was set to accept only ASCII characters /// -- or -- targetEncoding was set to accept UTF-8 characters /// Still, some characters have special meaning /// in TeX and have to be preceded with a backslash bool found = false; for (const QChar &encoderLaTeXProtectedSymbol : encoderLaTeXProtectedSymbols) if (encoderLaTeXProtectedSymbol == c) { output.append(QLatin1Char('\\')); found = true; break; } if (!found && !inMathMode) for (const QChar &encoderLaTeXProtectedTextOnlySymbol : encoderLaTeXProtectedTextOnlySymbols) if (encoderLaTeXProtectedTextOnlySymbol == c) { output.append(QLatin1Char('\\')); break; } /// Dump character to output output.append(c); /// Finally, check if input character is a dollar sign /// without a preceding backslash, means toggling between /// text mode and math mode if (c == QLatin1Char('$') && (i == 0 || input[i - 1] != QLatin1Char('\\'))) inMathMode = !inMathMode; } } output.squeeze(); return output; } int EncoderLaTeX::modifierInLookupTable(const QChar modifier) const { for (int m = 0; m < lookupTableNumModifiers && lookupTable[m] != nullptr; ++m) if (lookupTable[m]->modifier == modifier) return m; return -1; } QString EncoderLaTeX::readAlphaCharacters(const QString &base, int startFrom) const { const int len = base.size(); for (int j = startFrom; j < len; ++j) { if (!isAsciiLetter(base[j])) return base.mid(startFrom, j - startFrom); } return base.mid(startFrom); } const EncoderLaTeX &EncoderLaTeX::instance() { static const EncoderLaTeX self; return self; } diff --git a/src/io/encoderxml.cpp b/src/io/encoderxml.cpp index 15281701..4a9cf485 100644 --- a/src/io/encoderxml.cpp +++ b/src/io/encoderxml.cpp @@ -1,134 +1,134 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "encoderxml.h" #include <QStringList> #include <QList> static const struct EncoderXMLCharMapping { QChar unicode; const QString xml; } charmappingdataxml[] = { {QChar(0x0026), QStringLiteral("&amp;")}, {QChar(0x0022), QStringLiteral("&quot;")}, {QChar(0x003C), QStringLiteral("&lt;")}, {QChar(0x003E), QStringLiteral("&gt;")} }; /** * Private class to store internal variables that should not be visible * in the interface as defined in the header file. */ class EncoderXML::EncoderXMLPrivate { public: static const QStringList backslashSymbols; }; const QStringList EncoderXML::EncoderXMLPrivate::backslashSymbols {QStringLiteral("\\&"), QStringLiteral("\\%"), QStringLiteral("\\_")}; EncoderXML::EncoderXML() : Encoder(), d(new EncoderXML::EncoderXMLPrivate) { /// nothing } EncoderXML::~EncoderXML() { delete d; } QString EncoderXML::decode(const QString &text) const { QString result = text; for (const auto &item : charmappingdataxml) result.replace(item.xml, item.unicode); /** * Find and replace all characters written as hexadecimal number */ int p = -1; while ((p = result.indexOf(QStringLiteral("&#x"), p + 1)) >= 0) { int p2 = result.indexOf(QStringLiteral(";"), p + 1); if (p2 < 0 || p2 > p + 8) break; bool ok = false; int hex = result.midRef(p + 3, p2 - p - 3).toInt(&ok, 16); if (ok && hex > 0) result.replace(result.mid(p, p2 - p + 1), QChar(hex)); } /** * Find and replace all characters written as decimal number */ p = -1; while ((p = result.indexOf(QStringLiteral("&#"), p + 1)) >= 0) { int p2 = result.indexOf(QStringLiteral(";"), p + 1); if (p2 < 0 || p2 > p + 8) break; bool ok = false; int dec = result.midRef(p + 2, p2 - p - 2).toInt(&ok, 10); if (ok && dec > 0) result.replace(result.mid(p, p2 - p + 1), QChar(dec)); } /// Replace special symbols with backslash-encoded variant (& --> \&) for (const QString &backslashSymbol : EncoderXMLPrivate::backslashSymbols) { int p = -1; while ((p = result.indexOf(backslashSymbol[1], p + 1)) >= 0) { if (p == 0 || result[p - 1] != QLatin1Char('\\')) { /// replace only symbols which have no backslash on their right result = result.left(p) + QLatin1Char('\\') + result.mid(p); ++p; } } } return result; } QString EncoderXML::encode(const QString &text, const TargetEncoding targetEncoding) const { QString result = text; for (const auto &item : charmappingdataxml) result.replace(item.unicode, item.xml); - if (targetEncoding == TargetEncodingASCII) { + if (targetEncoding == TargetEncoding::ASCII) { /// Replace all problematic or non-ASCII characters (code < 32 or code > 127) /// with an entity code, for example a-umlaut becomes '&#228;'. for (int i = result.length() - 1; i >= 0; --i) { const auto code = result[i].unicode(); if (code < 32 || code > 127) result = result.left(i) + QStringLiteral("&#") + QString::number(code) + QStringLiteral(";") + result.mid(i + 1); } } /// Replace backlash-encoded symbols with plain text (\& --> &) for (const QString &backslashSymbol : EncoderXMLPrivate::backslashSymbols) { result.replace(backslashSymbol, backslashSymbol[1]); } return result; } const EncoderXML &EncoderXML::instance() { static const EncoderXML self; return self; } diff --git a/src/io/fileexporterbibtex.cpp b/src/io/fileexporterbibtex.cpp index f7a6e704..12408f45 100644 --- a/src/io/fileexporterbibtex.cpp +++ b/src/io/fileexporterbibtex.cpp @@ -1,676 +1,676 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileexporterbibtex.h" #include <typeinfo> #include <QTextCodec> #include <QTextStream> #include <QStringList> #include <QBuffer> #include <BibTeXEntries> #include <BibTeXFields> #include <Preferences> #include <File> #include <Element> #include <Entry> #include <Macro> #include <Preamble> #include <Value> #include <Comment> #include "encoderlatex.h" #include "textencoder.h" #include "logging_io.h" FileExporterBibTeX *FileExporterBibTeX::staticFileExporterBibTeX = nullptr; class FileExporterBibTeX::FileExporterBibTeXPrivate { private: FileExporterBibTeX *p; public: QChar stringOpenDelimiter; QChar stringCloseDelimiter; KBibTeX::Casing keywordCasing; Preferences::QuoteComment quoteComment; QString encoding, forcedEncoding; Qt::CheckState protectCasing; QString personNameFormatting; QString listSeparator; bool cancelFlag; QTextCodec *destinationCodec; FileExporterBibTeXPrivate(FileExporterBibTeX *parent) - : p(parent), keywordCasing(KBibTeX::cLowerCase), quoteComment(Preferences::qcNone), protectCasing(Qt::PartiallyChecked), cancelFlag(false), destinationCodec(nullptr) + : p(parent), keywordCasing(KBibTeX::Casing::LowerCase), quoteComment(Preferences::QuoteComment::None), protectCasing(Qt::PartiallyChecked), cancelFlag(false), destinationCodec(nullptr) { /// nothing } void loadState() { #ifdef HAVE_KF5 encoding = Preferences::instance().bibTeXEncoding(); QString stringDelimiter = Preferences::instance().bibTeXStringDelimiter(); if (stringDelimiter.length() != 2) stringDelimiter = Preferences::defaultBibTeXStringDelimiter; #else // HAVE_KF5 encoding = QStringLiteral("LaTeX"); const QString stringDelimiter = QStringLiteral("{}"); #endif // HAVE_KF5 stringOpenDelimiter = stringDelimiter[0]; stringCloseDelimiter = stringDelimiter[1]; #ifdef HAVE_KF5 keywordCasing = Preferences::instance().bibTeXKeywordCasing(); quoteComment = Preferences::instance().bibTeXQuoteComment(); protectCasing = Preferences::instance().bibTeXProtectCasing() ? Qt::Checked : Qt::Unchecked; listSeparator = Preferences::instance().bibTeXListSeparator(); #else // HAVE_KF5 - keywordCasing = KBibTeX::cLowerCase; + keywordCasing = KBibTeX::Casing::LowerCase; quoteComment = qcNone; protectCasing = Qt::PartiallyChecked; listSeparator = QStringLiteral("; "); #endif // HAVE_KF5 personNameFormatting = Preferences::instance().personNameFormat(); } void loadStateFromFile(const File *bibtexfile) { if (bibtexfile == nullptr) return; if (bibtexfile->hasProperty(File::Encoding)) encoding = bibtexfile->property(File::Encoding).toString(); if (!forcedEncoding.isEmpty()) encoding = forcedEncoding; applyEncoding(encoding); if (bibtexfile->hasProperty(File::StringDelimiter)) { QString stringDelimiter = bibtexfile->property(File::StringDelimiter).toString(); if (stringDelimiter.length() != 2) stringDelimiter = Preferences::defaultBibTeXStringDelimiter; stringOpenDelimiter = stringDelimiter[0]; stringCloseDelimiter = stringDelimiter[1]; } if (bibtexfile->hasProperty(File::QuoteComment)) quoteComment = static_cast<Preferences::QuoteComment>(bibtexfile->property(File::QuoteComment).toInt()); if (bibtexfile->hasProperty(File::KeywordCasing)) keywordCasing = static_cast<KBibTeX::Casing>(bibtexfile->property(File::KeywordCasing).toInt()); if (bibtexfile->hasProperty(File::ProtectCasing)) protectCasing = static_cast<Qt::CheckState>(bibtexfile->property(File::ProtectCasing).toInt()); if (bibtexfile->hasProperty(File::NameFormatting)) { /// if the user set "use global default", this property is an empty string /// in this case, keep default value const QString buffer = bibtexfile->property(File::NameFormatting).toString(); personNameFormatting = buffer.isEmpty() ? personNameFormatting : buffer; } if (bibtexfile->hasProperty(File::ListSeparator)) listSeparator = bibtexfile->property(File::ListSeparator).toString(); } bool writeEntry(QIODevice *iodevice, const Entry &entry) { /// write start of a entry (entry type and id) in plain ASCII iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(entry.type(), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(Encoder::instance().convertToPlainAscii(entry.id()).toLatin1()); for (Entry::ConstIterator it = entry.constBegin(); it != entry.constEnd(); ++it) { const QString key = it.key(); Value value = it.value(); if (value.isEmpty()) continue; ///< ignore empty key-value pairs - QString text = p->internalValueToBibTeX(value, key, leUTF8); + QString text = p->internalValueToBibTeX(value, key, UseLaTeXEncoding::UTF8); if (text.isEmpty()) { /// ignore empty key-value pairs qCWarning(LOG_KBIBTEX_IO) << "Value for field " << key << " is empty" << endl; continue; } // FIXME hack! const QSharedPointer<ValueItem> first = *value.constBegin(); if (PlainText::isPlainText(*first) && (key == Entry::ftTitle || key == Entry::ftBookTitle || key == Entry::ftSeries)) { if (protectCasing == Qt::Checked) addProtectiveCasing(text); else if (protectCasing == Qt::Unchecked) removeProtectiveCasing(text); } iodevice->putChar(','); iodevice->putChar('\n'); iodevice->putChar('\t'); iodevice->write(Encoder::instance().convertToPlainAscii(BibTeXFields::instance().format(key, keywordCasing)).toLatin1()); iodevice->putChar(' '); iodevice->putChar('='); iodevice->putChar(' '); iodevice->write(TextEncoder::encode(text, destinationCodec)); } iodevice->putChar('\n'); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } bool writeMacro(QIODevice *iodevice, const Macro &macro) { - QString text = p->internalValueToBibTeX(macro.value(), QString(), leUTF8); + QString text = p->internalValueToBibTeX(macro.value(), QString(), UseLaTeXEncoding::UTF8); if (protectCasing == Qt::Checked) addProtectiveCasing(text); else if (protectCasing == Qt::Unchecked) removeProtectiveCasing(text); iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("String"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(TextEncoder::encode(macro.key(), destinationCodec)); iodevice->putChar(' '); iodevice->putChar('='); iodevice->putChar(' '); iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } bool writeComment(QIODevice *iodevice, const Comment &comment) { QString text = comment.text() ; - if (comment.useCommand() || quoteComment == Preferences::qcCommand) { + if (comment.useCommand() || quoteComment == Preferences::QuoteComment::Command) { iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Comment"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); - } else if (quoteComment == Preferences::qcPercentSign) { + } else if (quoteComment == Preferences::QuoteComment::PercentSign) { QStringList commentLines = text.split('\n', QString::SkipEmptyParts); for (QStringList::Iterator it = commentLines.begin(); it != commentLines.end(); ++it) { const QByteArray line = TextEncoder::encode(*it, destinationCodec); if (line.length() == 0 || line[0] != QLatin1Char('%')) { /// Guarantee that every line starts with /// a percent sign iodevice->putChar('%'); } iodevice->write(line); iodevice->putChar('\n'); } iodevice->putChar('\n'); } else { iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('\n'); iodevice->putChar('\n'); } return true; } bool writePreamble(QIODevice *iodevice, const Preamble &preamble) { iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Preamble"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); /// Remember: strings from preamble do not get encoded, /// may contain raw LaTeX commands and code - iodevice->write(TextEncoder::encode(p->internalValueToBibTeX(preamble.value(), QString(), leRaw), destinationCodec)); + iodevice->write(TextEncoder::encode(p->internalValueToBibTeX(preamble.value(), QString(), UseLaTeXEncoding::Raw), destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } QString addProtectiveCasing(QString &text) { /// Check if either /// - text is too short (less than two characters) or /// - text neither starts/stops with double quotation marks /// nor starts with { and stops with } if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) { /// Nothing to protect, as this is no text string return text; } bool addBrackets = true; if (text[1] == QLatin1Char('{') && text[text.length() - 2] == QLatin1Char('}')) { /// If the given text looks like this: {{...}} or "{...}" /// still check that it is not like this: {{..}..{..}} addBrackets = false; for (int i = text.length() - 2, count = 0; !addBrackets && i > 1; --i) { if (text[i] == QLatin1Char('{')) ++count; else if (text[i] == QLatin1Char('}')) --count; if (count == 0) addBrackets = true; } } if (addBrackets) text.insert(1, QStringLiteral("{")).insert(text.length() - 1, QStringLiteral("}")); return text; } QString removeProtectiveCasing(QString &text) { /// Check if either /// - text is too short (less than two characters) or /// - text neither starts/stops with double quotation marks /// nor starts with { and stops with } if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) { /// Nothing to protect, as this is no text string return text; } if (text[1] != QLatin1Char('{') || text[text.length() - 2] != QLatin1Char('}')) /// Nothing to remove return text; /// If the given text looks like this: {{...}} or "{...}" /// still check that it is not like this: {{..}..{..}} bool removeBrackets = true; for (int i = text.length() - 2, count = 0; removeBrackets && i > 1; --i) { if (text[i] == QLatin1Char('{')) ++count; else if (text[i] == QLatin1Char('}')) --count; if (count == 0) removeBrackets = false; } if (removeBrackets) text.remove(text.length() - 2, 1).remove(1, 1); return text; } QString &protectQuotationMarks(QString &text) { int p = -1; while ((p = text.indexOf(QLatin1Char('"'), p + 1)) > 0) if (p == 0 || text[p - 1] != QLatin1Char('\\')) { text.insert(p + 1, QStringLiteral("}")).insert(p, QStringLiteral("{")); ++p; } return text; } void applyEncoding(QString &encoding) { encoding = encoding.isEmpty() ? QStringLiteral("latex") : encoding.toLower(); destinationCodec = QTextCodec::codecForName(encoding == QStringLiteral("latex") ? "us-ascii" : encoding.toLatin1()); } bool requiresPersonQuoting(const QString &text, bool isLastName) { if (isLastName && !text.contains(QChar(' '))) /** Last name contains NO spaces, no quoting necessary */ return false; else if (!isLastName && !text.contains(QStringLiteral(" and "))) /** First name contains no " and " no quoting necessary */ return false; else if (isLastName && !text.isEmpty() && text[0].isLower()) /** Last name starts with lower-case character (von, van, de, ...) */ // FIXME does not work yet return false; else if (text[0] != '{' || text[text.length() - 1] != '}') /** as either last name contains spaces or first name contains " and " and there is no protective quoting yet, there must be a protective quoting added */ return true; int bracketCounter = 0; for (int i = text.length() - 1; i >= 0; --i) { if (text[i] == '{') ++bracketCounter; else if (text[i] == '}') --bracketCounter; if (bracketCounter == 0 && i > 0) return true; } return false; } }; FileExporterBibTeX::FileExporterBibTeX(QObject *parent) : FileExporter(parent), d(new FileExporterBibTeXPrivate(this)) { /// nothing } FileExporterBibTeX::~FileExporterBibTeX() { delete d; } void FileExporterBibTeX::setEncoding(const QString &encoding) { d->forcedEncoding = encoding; } bool FileExporterBibTeX::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = true; const int totalElements = bibtexfile->count(); int currentPos = 0; d->loadState(); d->loadStateFromFile(bibtexfile); QBuffer outputBuffer; outputBuffer.open(QBuffer::WriteOnly); /// Memorize which entries are used in a crossref field QHash<QString, QStringList> crossRefMap; for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>(); if (!entry.isNull()) { const QString crossRef = PlainTextValue::text(entry->value(Entry::ftCrossRef)); if (!crossRef.isEmpty()) { QStringList crossRefList = crossRefMap.value(crossRef, QStringList()); crossRefList.append(entry->id()); crossRefMap.insert(crossRef, crossRefList); } } } bool allPreamblesAndMacrosProcessed = false; QSet<QString> processedEntryIds; for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Element> element = (*it); QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { processedEntryIds.insert(entry->id()); /// Postpone entries that are crossref'ed QStringList crossRefList = crossRefMap.value(entry->id(), QStringList()); if (!crossRefList.isEmpty()) { bool allProcessed = true; for (const QString &origin : crossRefList) allProcessed &= processedEntryIds.contains(origin); if (allProcessed) crossRefMap.remove(entry->id()); else continue; } if (!allPreamblesAndMacrosProcessed) { /// Guarantee that all macros and the preamble are written /// before the first entry (@article, ...) is written for (File::ConstIterator msit = it + 1; msit != bibtexfile->constEnd() && result && !d->cancelFlag; ++msit) { QSharedPointer<const Preamble> preamble = (*msit).dynamicCast<const Preamble>(); if (!preamble.isNull()) { result &= d->writePreamble(&outputBuffer, *preamble); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Macro> macro = (*msit).dynamicCast<const Macro>(); if (!macro.isNull()) { result &= d->writeMacro(&outputBuffer, *macro); emit progress(++currentPos, totalElements); } } } allPreamblesAndMacrosProcessed = true; } result &= d->writeEntry(&outputBuffer, *entry); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull() && !comment->text().startsWith(QStringLiteral("x-kbibtex-"))) { result &= d->writeComment(&outputBuffer, *comment); emit progress(++currentPos, totalElements); } else if (!allPreamblesAndMacrosProcessed) { QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) { result &= d->writePreamble(&outputBuffer, *preamble); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) { result &= d->writeMacro(&outputBuffer, *macro); emit progress(++currentPos, totalElements); } } } } } /// Crossref'ed entries are written last if (!crossRefMap.isEmpty()) for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>(); if (entry.isNull()) continue; if (!crossRefMap.contains(entry->id())) continue; result &= d->writeEntry(&outputBuffer, *entry); emit progress(++currentPos, totalElements); } outputBuffer.close(); ///< close writing operation outputBuffer.open(QBuffer::ReadOnly); const QByteArray outputData = outputBuffer.readAll(); outputBuffer.close(); bool hasNonAsciiCharacters = false; for (const unsigned char c : outputData) if ((c & 128) > 0) { hasNonAsciiCharacters = true; break; } if (hasNonAsciiCharacters) { const QString encoding = d->encoding.toLower() == QStringLiteral("latex") ? QStringLiteral("utf-8") : d->encoding; Comment encodingComment(QStringLiteral("x-kbibtex-encoding=") + encoding, true); result &= d->writeComment(iodevice, encodingComment); } iodevice->write(outputData); iodevice->close(); return result && !d->cancelFlag; } bool FileExporterBibTeX::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; d->loadState(); d->loadStateFromFile(bibtexfile); if (!d->forcedEncoding.isEmpty()) d->encoding = d->forcedEncoding; d->applyEncoding(d->encoding); const QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) result |= d->writeEntry(iodevice, *entry); else { const QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) result |= d->writeMacro(iodevice, *macro); else { const QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull()) result |= d->writeComment(iodevice, *comment); else { const QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) result |= d->writePreamble(iodevice, *preamble); } } } iodevice->close(); return result && !d->cancelFlag; } void FileExporterBibTeX::cancel() { d->cancelFlag = true; } QString FileExporterBibTeX::valueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding) { if (staticFileExporterBibTeX == nullptr) { staticFileExporterBibTeX = new FileExporterBibTeX(nullptr); staticFileExporterBibTeX->d->loadState(); } return staticFileExporterBibTeX->internalValueToBibTeX(value, key, useLaTeXEncoding); } QString FileExporterBibTeX::applyEncoder(const QString &input, UseLaTeXEncoding useLaTeXEncoding) const { switch (useLaTeXEncoding) { - case leLaTeX: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingASCII); - case leUTF8: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingUTF8); + case UseLaTeXEncoding::LaTeX: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncoding::ASCII); + case UseLaTeXEncoding::UTF8: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncoding::UTF8); default: return input; } } QString FileExporterBibTeX::internalValueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding) { if (value.isEmpty()) return QString(); QString result; bool isOpen = false; QSharedPointer<const ValueItem> prev; for (const auto &valueItem : value) { QSharedPointer<const MacroKey> macroKey = valueItem.dynamicCast<const MacroKey>(); if (!macroKey.isNull()) { if (isOpen) result.append(d->stringCloseDelimiter); isOpen = false; if (!result.isEmpty()) result.append(" # "); result.append(macroKey->text()); prev = macroKey; } else { QSharedPointer<const PlainText> plainText = valueItem.dynamicCast<const PlainText>(); if (!plainText.isNull()) { QString textBody = applyEncoder(plainText->text(), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const PlainText>().isNull()) result.append(' '); else if (!prev.dynamicCast<const Person>().isNull()) { /// handle "et al." i.e. "and others" result.append(" and "); } else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = plainText; } else { QSharedPointer<const VerbatimText> verbatimText = valueItem.dynamicCast<const VerbatimText>(); if (!verbatimText.isNull()) { QString textBody = verbatimText->text(); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const VerbatimText>().isNull()) { const QString keyToLower(key.toLower()); if (keyToLower.startsWith(Entry::ftUrl) || keyToLower.startsWith(Entry::ftLocalFile) || keyToLower.startsWith(Entry::ftFile) || keyToLower.startsWith(Entry::ftDOI)) /// Filenames and alike have be separated by a semicolon, /// as a plain comma may be part of the filename or URL result.append(QStringLiteral("; ")); else result.append(' '); } else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = verbatimText; } else { QSharedPointer<const Person> person = valueItem.dynamicCast<const Person>(); if (!person.isNull()) { QString firstName = person->firstName(); if (!firstName.isEmpty() && d->requiresPersonQuoting(firstName, false)) firstName = firstName.prepend("{").append("}"); QString lastName = person->lastName(); if (!lastName.isEmpty() && d->requiresPersonQuoting(lastName, true)) lastName = lastName.prepend("{").append("}"); QString suffix = person->suffix(); /// Fall back and enforce comma-based name formatting /// if name contains a suffix like "Jr." /// Otherwise name could not be parsed again reliable const QString pnf = suffix.isEmpty() ? d->personNameFormatting : Preferences::personNameFormatLastFirst; QString thisName = applyEncoder(Person::transcribePersonName(pnf, firstName, lastName, suffix), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const Person>().isNull()) result.append(" and "); else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(thisName); result.append(thisName); prev = person; } else { QSharedPointer<const Keyword> keyword = valueItem.dynamicCast<const Keyword>(); if (!keyword.isNull()) { QString textBody = applyEncoder(keyword->text(), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const Keyword>().isNull()) result.append(d->listSeparator); else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = keyword; } } } } } prev = valueItem; } if (isOpen) result.append(d->stringCloseDelimiter); return result; } bool FileExporterBibTeX::isFileExporterBibTeX(const FileExporter &other) { return typeid(other) == typeid(FileExporterBibTeX); } diff --git a/src/io/fileexporterbibtex.h b/src/io/fileexporterbibtex.h index 9f37dda2..c136f3e6 100644 --- a/src/io/fileexporterbibtex.h +++ b/src/io/fileexporterbibtex.h @@ -1,78 +1,78 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEEXPORTERBIBTEX_H #define KBIBTEX_IO_FILEEXPORTERBIBTEX_H #include <QTextStream> #include <Element> #include <Value> #include <FileExporter> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 class QChar; class Comment; class Preamble; class Macro; class Entry; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXIO_EXPORT FileExporterBibTeX : public FileExporter { Q_OBJECT public: - enum UseLaTeXEncoding {leUTF8, leLaTeX, leRaw}; + enum class UseLaTeXEncoding {UTF8, LaTeX, Raw}; explicit FileExporterBibTeX(QObject *parent); ~FileExporterBibTeX() override; void setEncoding(const QString &encoding); bool save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog = nullptr) override; bool save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog = nullptr) override; - static QString valueToBibTeX(const Value &value, const QString &fieldType = QString(), UseLaTeXEncoding useLaTeXEncoding = leLaTeX); + static QString valueToBibTeX(const Value &value, const QString &fieldType = QString(), UseLaTeXEncoding useLaTeXEncoding = UseLaTeXEncoding::LaTeX); /** * Cheap and fast test if another FileExporter is a FileExporterBibTeX object. * @param other another FileExporter object to test * @return true if FileExporter is actually a FileExporterBibTeX */ static bool isFileExporterBibTeX(const FileExporter &other); public slots: void cancel() override; private: class FileExporterBibTeXPrivate; FileExporterBibTeXPrivate *d; inline QString applyEncoder(const QString &input, UseLaTeXEncoding useLaTeXEncoding) const; - QString internalValueToBibTeX(const Value &value, const QString &fieldType = QString(), UseLaTeXEncoding useLaTeXEncoding = leLaTeX); + QString internalValueToBibTeX(const Value &value, const QString &fieldType = QString(), UseLaTeXEncoding useLaTeXEncoding = UseLaTeXEncoding::LaTeX); static FileExporterBibTeX *staticFileExporterBibTeX; }; #endif // KBIBTEX_IO_FILEEXPORTERBIBTEX_H diff --git a/src/io/fileexporterbibtexoutput.cpp b/src/io/fileexporterbibtexoutput.cpp index e822e85f..fd1f72c3 100644 --- a/src/io/fileexporterbibtexoutput.cpp +++ b/src/io/fileexporterbibtexoutput.cpp @@ -1,139 +1,139 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileexporterbibtexoutput.h" #include <QBuffer> #include <QFile> #include <QDir> #include <QStringList> #include <QUrl> #include <QTextStream> #include <KBibTeX> #include <Preferences> #include <Element> #include <Entry> #include "fileexporterbibtex.h" #include "logging_io.h" FileExporterBibTeXOutput::FileExporterBibTeXOutput(OutputType outputType, QObject *parent) : FileExporterToolchain(parent), m_outputType(outputType) { m_fileBasename = QStringLiteral("bibtex-to-output"); m_fileStem = tempDir.path() + QDir::separator() + m_fileBasename; } FileExporterBibTeXOutput::~FileExporterBibTeXOutput() { /// nothing } bool FileExporterBibTeXOutput::save(QIODevice *ioDevice, const File *bibtexfile, QStringList *errorLog) { if (!ioDevice->isWritable() && !ioDevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; QFile bibTeXFile(m_fileStem + KBibTeX::extensionBibTeX); if (bibTeXFile.open(QIODevice::WriteOnly)) { FileExporterBibTeX bibtexExporter(this); bibtexExporter.setEncoding(QStringLiteral("utf-8")); result = bibtexExporter.save(&bibTeXFile, bibtexfile, errorLog); bibTeXFile.close(); } if (result) result = generateOutput(errorLog); if (result) - result = writeFileToIODevice(m_fileStem + (m_outputType == BibTeXLogFile ? KBibTeX::extensionBLG : KBibTeX::extensionBBL), ioDevice, errorLog); + result = writeFileToIODevice(m_fileStem + (m_outputType == OutputType::BibTeXLogFile ? KBibTeX::extensionBLG : KBibTeX::extensionBBL), ioDevice, errorLog); ioDevice->close(); return result; } bool FileExporterBibTeXOutput::save(QIODevice *ioDevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { if (!ioDevice->isWritable() && !ioDevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; QFile bibTeXFile(m_fileStem + KBibTeX::extensionBibTeX); if (bibTeXFile.open(QIODevice::WriteOnly)) { FileExporterBibTeX bibtexExporter(this); bibtexExporter.setEncoding(QStringLiteral("utf-8")); result = bibtexExporter.save(&bibTeXFile, element, bibtexfile, errorLog); bibTeXFile.close(); } if (result) result = generateOutput(errorLog); if (result) - result = writeFileToIODevice(m_fileStem + (m_outputType == BibTeXLogFile ? KBibTeX::extensionBLG : KBibTeX::extensionBBL), ioDevice, errorLog); + result = writeFileToIODevice(m_fileStem + (m_outputType == OutputType::BibTeXLogFile ? KBibTeX::extensionBLG : KBibTeX::extensionBBL), ioDevice, errorLog); ioDevice->close(); return result; } bool FileExporterBibTeXOutput::generateOutput(QStringList *errorLog) { QStringList cmdLines {QStringLiteral("pdflatex -halt-on-error ") + m_fileBasename + KBibTeX::extensionTeX, QStringLiteral("bibtex ") + m_fileBasename + KBibTeX::extensionAux}; if (writeLatexFile(m_fileStem + KBibTeX::extensionTeX) && runProcesses(cmdLines, errorLog)) return true; else { qCWarning(LOG_KBIBTEX_IO) << "Generating BibTeX output failed"; return false; } } bool FileExporterBibTeXOutput::writeLatexFile(const QString &filename) { QFile latexFile(filename); if (latexFile.open(QIODevice::WriteOnly)) { QTextStream ts(&latexFile); ts.setCodec("UTF-8"); ts << "\\documentclass{article}\n"; ts << "\\usepackage[T1]{fontenc}\n"; ts << "\\usepackage[utf8]{inputenc}\n"; if (kpsewhich(QStringLiteral("babel.sty"))) ts << "\\usepackage[" << Preferences::instance().laTeXBabelLanguage() << "]{babel}\n"; if (kpsewhich(QStringLiteral("hyperref.sty"))) ts << "\\usepackage[pdfproducer={KBibTeX: https://userbase.kde.org/KBibTeX},pdftex]{hyperref}\n"; else if (kpsewhich(QStringLiteral("url.sty"))) ts << "\\usepackage{url}\n"; const QString latexBibStyle = Preferences::instance().bibTeXBibliographyStyle(); if (latexBibStyle.startsWith(QStringLiteral("apacite")) && kpsewhich(QStringLiteral("apacite.sty"))) ts << "\\usepackage[bibnewpage]{apacite}\n"; ts << "\\bibliographystyle{" << latexBibStyle << "}\n"; ts << "\\begin{document}\n"; ts << "\\nocite{*}\n"; ts << QStringLiteral("\\bibliography{") + m_fileBasename + QStringLiteral("}\n"); ts << "\\end{document}\n"; latexFile.close(); return true; } else return false; } diff --git a/src/io/fileexporterbibtexoutput.h b/src/io/fileexporterbibtexoutput.h index 0ad873de..87c86bc7 100644 --- a/src/io/fileexporterbibtexoutput.h +++ b/src/io/fileexporterbibtexoutput.h @@ -1,53 +1,53 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEEXPORTERBIBTEXOUTPUT_H #define KBIBTEX_IO_FILEEXPORTERBIBTEXOUTPUT_H #include <QStringList> #include <FileExporterToolchain> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 /** @author Thomas Fischer */ class KBIBTEXIO_EXPORT FileExporterBibTeXOutput : public FileExporterToolchain { Q_OBJECT public: - enum OutputType {BibTeXLogFile, BibTeXBlockList}; + enum class OutputType {BibTeXLogFile, BibTeXBlockList}; explicit FileExporterBibTeXOutput(OutputType outputType, QObject *parent); ~FileExporterBibTeXOutput() override; bool save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog = nullptr) override; bool save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog = nullptr) override; private: OutputType m_outputType; QString m_fileBasename; QString m_fileStem; bool generateOutput(QStringList *errorLog); bool writeLatexFile(const QString &filename); }; #endif // KBIBTEX_IO_FILEEXPORTERBIBTEXOUTPUT_H diff --git a/src/io/fileexporterbibutils.cpp b/src/io/fileexporterbibutils.cpp index 4fbf3150..687d7e6d 100644 --- a/src/io/fileexporterbibutils.cpp +++ b/src/io/fileexporterbibutils.cpp @@ -1,84 +1,84 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileexporterbibutils.h" #include <QBuffer> #include "fileexporterbibtex.h" #include "logging_io.h" class FileExporterBibUtils::Private { private: // UNUSED FileExporterBibUtils *p; public: FileExporterBibTeX *bibtexExporter; Private(FileExporterBibUtils *parent) // UNUSED : p(parent) { bibtexExporter = new FileExporterBibTeX(parent); bibtexExporter->setEncoding(QStringLiteral("utf-8")); } ~Private() { delete bibtexExporter; } }; FileExporterBibUtils::FileExporterBibUtils(QObject *parent) : FileExporter(parent), BibUtils(), d(new FileExporterBibUtils::Private(this)) { // TODO } FileExporterBibUtils::~FileExporterBibUtils() { delete d; } bool FileExporterBibUtils::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog) { if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } QBuffer buffer; bool result = d->bibtexExporter->save(&buffer, bibtexfile, errorLog); if (result) - result = convert(buffer, BibUtils::BibTeX, *iodevice, format()); + result = convert(buffer, BibUtils::Format::BibTeX, *iodevice, format()); iodevice->close(); return result; } bool FileExporterBibUtils::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) return false; QBuffer buffer; bool result = d->bibtexExporter->save(&buffer, element, bibtexfile, errorLog); if (result) - result = convert(buffer, BibUtils::BibTeX, *iodevice, format()); + result = convert(buffer, BibUtils::Format::BibTeX, *iodevice, format()); iodevice->close(); return result; } diff --git a/src/io/fileexporterpdf.cpp b/src/io/fileexporterpdf.cpp index 8376c7f0..6560429f 100644 --- a/src/io/fileexporterpdf.cpp +++ b/src/io/fileexporterpdf.cpp @@ -1,202 +1,202 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileexporterpdf.h" #include <QFile> #include <QStringList> #include <QUrl> #include <QTextStream> #include <QDir> #include <KBibTeX> #include <Preferences> #include <Element> #include <Entry> #include "fileinfo.h" #include "fileexporterbibtex.h" #include "logging_io.h" FileExporterPDF::FileExporterPDF(QObject *parent) : FileExporterToolchain(parent) { m_fileBasename = QStringLiteral("bibtex-to-pdf"); m_fileStem = tempDir.path() + QDir::separator() + m_fileBasename; - setFileEmbedding(FileExporterPDF::EmbedBibTeXFileAndReferences); + setFileEmbedding(FileExporterPDF::FileEmbedding::BibTeXFileAndReferences); } FileExporterPDF::~FileExporterPDF() { /// nothing } bool FileExporterPDF::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog) { if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; m_embeddedFileList.clear(); - if (m_fileEmbedding & EmbedBibTeXFile) + if (m_fileEmbeddings.testFlag(FileEmbedding::BibTeXFile)) m_embeddedFileList.append(QString(QStringLiteral("%1|%2|%3")).arg(QStringLiteral("BibTeX source"), m_fileStem + KBibTeX::extensionBibTeX, m_fileBasename + KBibTeX::extensionBibTeX)); - if (m_fileEmbedding & EmbedReferences) + if (m_fileEmbeddings.testFlag(FileEmbedding::References)) fillEmbeddedFileList(bibtexfile); QFile output(m_fileStem + KBibTeX::extensionBibTeX); if (output.open(QIODevice::WriteOnly)) { FileExporterBibTeX bibtexExporter(this); bibtexExporter.setEncoding(QStringLiteral("latex")); result = bibtexExporter.save(&output, bibtexfile, errorLog); output.close(); } if (result) result = generatePDF(iodevice, errorLog); if (errorLog != nullptr) qCDebug(LOG_KBIBTEX_IO) << "errorLog" << errorLog->join(QStringLiteral(";")); iodevice->close(); return result; } bool FileExporterPDF::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; m_embeddedFileList.clear(); //if (m_fileEmbedding & EmbedReferences) // FIXME need File object fillEmbeddedFileList(element); QFile output(m_fileStem + KBibTeX::extensionBibTeX); if (output.open(QIODevice::WriteOnly)) { FileExporterBibTeX bibtexExporter(this); bibtexExporter.setEncoding(QStringLiteral("latex")); result = bibtexExporter.save(&output, element, bibtexfile, errorLog); output.close(); } if (result) result = generatePDF(iodevice, errorLog); iodevice->close(); return result; } void FileExporterPDF::setDocumentSearchPaths(const QStringList &searchPaths) { m_searchPaths = searchPaths; } -void FileExporterPDF::setFileEmbedding(FileEmbedding fileEmbedding) { +void FileExporterPDF::setFileEmbedding(const FileEmbeddings fileEmbedding) { /// If there is not embedfile.sty file, disable embedding /// irrespective of user's wishes if (!kpsewhich(QStringLiteral("embedfile.sty"))) - m_fileEmbedding = NoFileEmbedding; + m_fileEmbeddings = FileEmbedding::None; else - m_fileEmbedding = fileEmbedding; + m_fileEmbeddings = fileEmbedding; } bool FileExporterPDF::generatePDF(QIODevice *iodevice, QStringList *errorLog) { QStringList cmdLines {QStringLiteral("pdflatex -halt-on-error ") + m_fileStem + KBibTeX::extensionTeX, QStringLiteral("bibtex ") + m_fileStem + KBibTeX::extensionAux, QStringLiteral("pdflatex -halt-on-error ") + m_fileStem + KBibTeX::extensionTeX, QStringLiteral("pdflatex -halt-on-error ") + m_fileStem + KBibTeX::extensionTeX}; return writeLatexFile(m_fileStem + KBibTeX::extensionTeX) && runProcesses(cmdLines, errorLog) && writeFileToIODevice(m_fileStem + KBibTeX::extensionPDF, iodevice, errorLog); } bool FileExporterPDF::writeLatexFile(const QString &filename) { QFile latexFile(filename); if (latexFile.open(QIODevice::WriteOnly)) { QTextStream ts(&latexFile); ts.setCodec("UTF-8"); ts << "\\documentclass{article}" << endl; ts << "\\usepackage[T1]{fontenc}" << endl; ts << "\\usepackage[utf8]{inputenc}" << endl; if (kpsewhich(QStringLiteral("babel.sty"))) ts << "\\usepackage[" << Preferences::instance().laTeXBabelLanguage() << "]{babel}" << endl; if (kpsewhich(QStringLiteral("hyperref.sty"))) ts << "\\usepackage[pdfborder={0 0 0},pdfproducer={KBibTeX: https://userbase.kde.org/KBibTeX},pdftex]{hyperref}" << endl; else if (kpsewhich(QStringLiteral("url.sty"))) ts << "\\usepackage{url}" << endl; const QString bibliographyStyle = Preferences::instance().bibTeXBibliographyStyle(); if (bibliographyStyle.startsWith(QStringLiteral("apacite")) && kpsewhich(QStringLiteral("apacite.sty"))) ts << "\\usepackage[bibnewpage]{apacite}" << endl; if ((bibliographyStyle == QStringLiteral("agsm") || bibliographyStyle == QStringLiteral("dcu") || bibliographyStyle == QStringLiteral("jmr") || bibliographyStyle == QStringLiteral("jphysicsB") || bibliographyStyle == QStringLiteral("kluwer") || bibliographyStyle == QStringLiteral("nederlands") || bibliographyStyle == QStringLiteral("dcu") || bibliographyStyle == QStringLiteral("dcu")) && kpsewhich(QStringLiteral("harvard.sty")) && kpsewhich(QStringLiteral("html.sty"))) ts << "\\usepackage{html}" << endl << "\\usepackage[dcucite]{harvard}" << endl << "\\renewcommand{\\harvardurl}{URL: \\url}" << endl; if (kpsewhich(QStringLiteral("embedfile.sty"))) ts << "\\usepackage{embedfile}" << endl; if (kpsewhich(QStringLiteral("geometry.sty"))) ts << "\\usepackage[paper=" << pageSizeToLaTeXName(Preferences::instance().pageSize()) << "]{geometry}" << endl; ts << "\\bibliographystyle{" << bibliographyStyle << "}" << endl; ts << "\\begin{document}" << endl; if (!m_embeddedFileList.isEmpty()) for (const QString &embeddedFile : const_cast<const QStringList &>(m_embeddedFileList)) { const QStringList param = embeddedFile.split(QStringLiteral("|")); QFile file(param[1]); if (file.exists()) ts << "\\embedfile[desc={" << param[0] << "}"; ts << ",filespec={" << param[2] << "}"; if (param[2].endsWith(KBibTeX::extensionBibTeX)) ts << ",mimetype={text/x-bibtex}"; else if (param[2].endsWith(KBibTeX::extensionPDF)) ts << ",mimetype={application/pdf}"; ts << "]{" << param[1] << "}" << endl; } ts << "\\nocite{*}" << endl; ts << QStringLiteral("\\bibliography{") << m_fileBasename << QStringLiteral("}") << endl; ts << "\\end{document}" << endl; latexFile.close(); return true; } else return false; } void FileExporterPDF::fillEmbeddedFileList(const File *bibtexfile) { for (const auto &element : const_cast<const File &>(*bibtexfile)) fillEmbeddedFileList(element, bibtexfile); } void FileExporterPDF::fillEmbeddedFileList(const QSharedPointer<const Element> element, const File *bibtexfile) { if (bibtexfile == nullptr || !bibtexfile->hasProperty(File::Url)) { /// If no valid File was provided or File is not saved, do not append files return; } const QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { const QString title = PlainTextValue::text(entry->value(Entry::ftTitle)); - const auto urlList = FileInfo::entryUrls(entry, bibtexfile->property(File::Url).toUrl(), FileInfo::TestExistenceYes); + const auto urlList = FileInfo::entryUrls(entry, bibtexfile->property(File::Url).toUrl(), FileInfo::TestExistence::Yes); for (const QUrl &url : urlList) { if (!url.isLocalFile()) continue; const QString filename = url.toLocalFile(); const QString basename = QFileInfo(filename).fileName(); m_embeddedFileList.append(QString(QStringLiteral("%1|%2|%3")).arg(title, filename, basename)); } } } diff --git a/src/io/fileexporterpdf.h b/src/io/fileexporterpdf.h index ca70d655..dec157d3 100644 --- a/src/io/fileexporterpdf.h +++ b/src/io/fileexporterpdf.h @@ -1,60 +1,67 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEEXPORTERPDF_H #define KBIBTEX_IO_FILEEXPORTERPDF_H #include <QStringList> #include <FileExporterToolchain> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 /** @author Thomas Fischer */ class KBIBTEXIO_EXPORT FileExporterPDF : public FileExporterToolchain { Q_OBJECT public: - enum FileEmbedding { NoFileEmbedding = 0, EmbedBibTeXFile = 1, EmbedReferences = 2, EmbedBibTeXFileAndReferences = EmbedBibTeXFile | EmbedReferences}; + enum class FileEmbedding { + None = 0, + BibTeXFile = 1, + References = 2, + BibTeXFileAndReferences = BibTeXFile | References + }; + Q_DECLARE_FLAGS(FileEmbeddings, FileEmbedding) + explicit FileExporterPDF(QObject *parent); ~FileExporterPDF() override; bool save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog = nullptr) override; bool save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog = nullptr) override; void setDocumentSearchPaths(const QStringList &searchPaths); - void setFileEmbedding(FileEmbedding fileEmbedding); + void setFileEmbedding(const FileEmbeddings fileEmbedding); private: QString m_fileBasename; QString m_fileStem; - FileEmbedding m_fileEmbedding; + FileEmbeddings m_fileEmbeddings; QStringList m_embeddedFileList; QStringList m_searchPaths; bool generatePDF(QIODevice *iodevice, QStringList *errorLog); bool writeLatexFile(const QString &filename); void fillEmbeddedFileList(const File *bibtexfile); void fillEmbeddedFileList(const QSharedPointer<const Element> element, const File *bibtexfile); }; #endif // KBIBTEX_IO_FILEEXPORTERPDF_H diff --git a/src/io/fileexporterxml.cpp b/src/io/fileexporterxml.cpp index eafe0f7e..de54a388 100644 --- a/src/io/fileexporterxml.cpp +++ b/src/io/fileexporterxml.cpp @@ -1,262 +1,262 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileexporterxml.h" #include <QRegularExpression> #include <QStringList> #include <KBibTeX> #include <File> #include <Entry> #include <Macro> #include <Comment> #include "encoderxml.h" #include "logging_io.h" FileExporterXML::FileExporterXML(QObject *parent) : FileExporter(parent), m_cancelFlag(false) { /// nothing } FileExporterXML::~FileExporterXML() { /// nothing } bool FileExporterXML::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = true; m_cancelFlag = false; QTextStream stream(iodevice); stream.setCodec("UTF-8"); stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << endl; stream << "<!-- XML document written by KBibTeXIO as part of KBibTeX -->" << endl; stream << "<!-- https://userbase.kde.org/KBibTeX -->" << endl; stream << "<bibliography>" << endl; for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !m_cancelFlag; ++it) result &= write(stream, (*it).data(), bibtexfile); stream << "</bibliography>" << endl; iodevice->close(); return result && !m_cancelFlag; } bool FileExporterXML::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(bibtexfile) Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } QTextStream stream(iodevice); stream.setCodec("UTF-8"); stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << endl; stream << "<!-- XML document written by KBibTeXIO as part of KBibTeX -->" << endl; stream << "<!-- https://userbase.kde.org/KBibTeX -->" << endl; stream << "<bibliography>" << endl; const bool result = write(stream, element.data()); stream << "</bibliography>" << endl; iodevice->close(); return result; } void FileExporterXML::cancel() { m_cancelFlag = true; } bool FileExporterXML::write(QTextStream &stream, const Element *element, const File *bibtexfile) { bool result = false; const Entry *entry = dynamic_cast<const Entry *>(element); if (entry != nullptr) { if (bibtexfile == nullptr) result |= writeEntry(stream, entry); else { QScopedPointer<const Entry> resolvedEntry(entry->resolveCrossref(bibtexfile)); result |= writeEntry(stream, resolvedEntry.data()); } } else { const Macro *macro = dynamic_cast<const Macro *>(element); if (macro != nullptr) result |= writeMacro(stream, macro); else { const Comment *comment = dynamic_cast<const Comment *>(element); if (comment != nullptr) result |= writeComment(stream, comment); else { // preambles are ignored, make no sense in XML files } } } return result; } bool FileExporterXML::writeEntry(QTextStream &stream, const Entry *entry) { - stream << " <entry id=\"" << EncoderXML::instance().encode(entry->id(), Encoder::TargetEncodingUTF8) << "\" type=\"" << entry->type().toLower() << "\">" << endl; + stream << " <entry id=\"" << EncoderXML::instance().encode(entry->id(), Encoder::TargetEncoding::UTF8) << "\" type=\"" << entry->type().toLower() << "\">" << endl; for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { const QString key = it.key().toLower(); const Value value = it.value(); if (key == Entry::ftAuthor || key == Entry::ftEditor) { Value internal = value; Value::ConstIterator lastIt = internal.constEnd(); --lastIt; const QSharedPointer<ValueItem> last = *lastIt; stream << " <" << key << "s"; if (!value.isEmpty() && PlainText::isPlainText(*last)) { QSharedPointer<const PlainText> pt = internal.last().staticCast<const PlainText>(); if (pt->text() == QStringLiteral("others")) { internal.erase(internal.end() - 1); stream << " etal=\"true\""; } } stream << ">" << endl; stream << valueToXML(internal, key) << endl; stream << " </" << key << "s>" << endl; } else if (key == Entry::ftAbstract) { static const QRegularExpression abstractRegExp(QStringLiteral("\\bAbstract[:]?([ ]|&nbsp;|&amp;nbsp;)*"), QRegularExpression::CaseInsensitiveOption); /// clean up HTML artifacts QString text = valueToXML(value); text = text.remove(abstractRegExp); stream << " <" << key << ">" << text << "</" << key << ">" << endl; } else if (key == Entry::ftMonth) { stream << " <month"; bool ok = false; int month = -1; QString tag; QString content; for (const auto &valueItem : value) { QSharedPointer<const MacroKey> macro = valueItem.dynamicCast<const MacroKey>(); if (!macro.isNull()) for (int i = 0; i < 12; i++) { if (QString::compare(macro->text(), KBibTeX::MonthsTriple[ i ]) == 0) { if (month < 1) { tag = KBibTeX::MonthsTriple[ i ]; month = i + 1; } content.append(KBibTeX::Months[ i ]); ok = true; break; } } else content.append(PlainTextValue::text(valueItem)); } if (!ok) content = valueToXML(value) ; if (!tag.isEmpty()) stream << " tag=\"" << key << "\""; if (month > 0) stream << " month=\"" << month << "\""; stream << '>' << content; stream << "</month>" << endl; } else { stream << " <" << key << ">" << valueToXML(value) << "</" << key << ">" << endl; } } stream << " </entry>" << endl; return true; } bool FileExporterXML::writeMacro(QTextStream &stream, const Macro *macro) { stream << " <string key=\"" << macro->key() << "\">"; stream << valueToXML(macro->value()); stream << "</string>" << endl; return true; } bool FileExporterXML::writeComment(QTextStream &stream, const Comment *comment) { stream << " <comment>" ; - stream << EncoderXML::instance().encode(comment->text(), Encoder::TargetEncodingUTF8); + stream << EncoderXML::instance().encode(comment->text(), Encoder::TargetEncoding::UTF8); stream << "</comment>" << endl; return true; } QString FileExporterXML::valueToXML(const Value &value, const QString &) { QString result; bool isFirst = true; for (const auto &valueItem : value) { if (!isFirst) result.append(' '); isFirst = false; QSharedPointer<const PlainText> plainText = valueItem.dynamicCast<const PlainText>(); if (!plainText.isNull()) - result.append("<text>" + cleanXML(EncoderXML::instance().encode(PlainTextValue::text(valueItem), Encoder::TargetEncodingUTF8)) + "</text>"); + result.append("<text>" + cleanXML(EncoderXML::instance().encode(PlainTextValue::text(valueItem), Encoder::TargetEncoding::UTF8)) + "</text>"); else { QSharedPointer<const Person> p = valueItem.dynamicCast<const Person>(); if (!p.isNull()) { result.append("<person>"); if (!p->firstName().isEmpty()) - result.append("<firstname>" + cleanXML(EncoderXML::instance().encode(p->firstName(), Encoder::TargetEncodingUTF8)) + "</firstname>"); + result.append("<firstname>" + cleanXML(EncoderXML::instance().encode(p->firstName(), Encoder::TargetEncoding::UTF8)) + "</firstname>"); if (!p->lastName().isEmpty()) - result.append("<lastname>" + cleanXML(EncoderXML::instance().encode(p->lastName(), Encoder::TargetEncodingUTF8)) + "</lastname>"); + result.append("<lastname>" + cleanXML(EncoderXML::instance().encode(p->lastName(), Encoder::TargetEncoding::UTF8)) + "</lastname>"); if (!p->suffix().isEmpty()) - result.append("<suffix>" + cleanXML(EncoderXML::instance().encode(p->suffix(), Encoder::TargetEncodingUTF8)) + "</suffix>"); + result.append("<suffix>" + cleanXML(EncoderXML::instance().encode(p->suffix(), Encoder::TargetEncoding::UTF8)) + "</suffix>"); result.append("</person>"); } // TODO: Other data types else - result.append("<text>" + cleanXML(EncoderXML::instance().encode(PlainTextValue::text(valueItem), Encoder::TargetEncodingUTF8)) + "</text>"); + result.append("<text>" + cleanXML(EncoderXML::instance().encode(PlainTextValue::text(valueItem), Encoder::TargetEncoding::UTF8)) + "</text>"); } } return result; } QString FileExporterXML::cleanXML(const QString &text) { static const QRegularExpression removal(QStringLiteral("[{}]+")); static const QRegularExpression lineBreaksRegExp(QStringLiteral("[ \\t]*[\\n\\r]")); QString result = text; result = result.replace(lineBreaksRegExp, QStringLiteral("<br/>")).remove(removal).remove(QStringLiteral("\\ensuremath")); return result; } diff --git a/src/io/fileimporter.h b/src/io/fileimporter.h index 68d8e083..04557250 100644 --- a/src/io/fileimporter.h +++ b/src/io/fileimporter.h @@ -1,114 +1,114 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEIMPORTER_H #define KBIBTEX_IO_FILEIMPORTER_H #include <QObject> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 class QIODevice; class File; class Person; /** @author Thomas Fischer */ class KBIBTEXIO_EXPORT FileImporter : public QObject { Q_OBJECT public: - enum MessageSeverity { - SeverityInfo, ///< Messages that are of informative type, such as additional comma for last key-value pair in BibTeX entry - SeverityWarning, ///< Messages that are of warning type, such as automatic corrections of BibTeX code without loss of information - SeverityError ///< Messages that are of error type, which point to issue where information may get lost, e.g. invalid syntax or incomplete data + enum class MessageSeverity { + Info, ///< Messages that are of informative type, such as additional comma for last key-value pair in BibTeX entry + Warning, ///< Messages that are of warning type, such as automatic corrections of BibTeX code without loss of information + Error ///< Messages that are of error type, which point to issue where information may get lost, e.g. invalid syntax or incomplete data }; explicit FileImporter(QObject *parent); ~FileImporter() override; File *fromString(const QString &text); virtual File *load(QIODevice *iodevice) = 0; /** * When importing data, show a dialog where the user may select options on the * import process such as selecting encoding. Re-implementing this function is * optional and should only be done if user interaction is necessary at import * actions. * Return true if the configuration step was successful and the application * may proceed. If returned false, the import process has to be stopped. * The importer may store configurations done here for future use (e.g. set default * values based on user input). * A calling application should call this function before calling load() or similar * functions. * The implementer may choose to show or not show a dialog, depending on e.g. if * additional information is necessary or not. */ virtual bool showImportDialog(QWidget *parent) { Q_UNUSED(parent) return true; } static bool guessCanDecode(const QString &) { return false; } /** * Split a person's name into its parts and construct a Person object from them. * This is a rather general functions and takes e.g. the curly brackets used in * (La)TeX not into account. * @param name The persons name * @return A Person object containing the name * @see Person */ static Person *splitName(const QString &name); private: static bool looksLikeSuffix(const QString &suffix); signals: void progress(int current, int total); /** * Signal to notify the user of a FileImporter class about issues detected * during loading and parsing bibliographic data. Messages may be of various * severity levels. The message text may reveal additional information on * what the issue is and where it has been found (e.g. line number). * Implementations of FileImporter are recommended to print a similar message * as debug output. * TODO messages shall get i18n'ized if the code is compiled with/linked against * KDE Frameworks libraries. * * @param severity The message's severity level * @param messageText The message's text */ void message(const FileImporter::MessageSeverity severity, const QString &messageText); public slots: virtual void cancel() { // nothing } }; Q_DECLARE_METATYPE(FileImporter::MessageSeverity) #endif // KBIBTEX_IO_FILEIMPORTER_H diff --git a/src/io/fileimporterbibtex.cpp b/src/io/fileimporterbibtex.cpp index f0fb8e07..fe4ea234 100644 --- a/src/io/fileimporterbibtex.cpp +++ b/src/io/fileimporterbibtex.cpp @@ -1,1327 +1,1327 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileimporterbibtex.h" #include <QTextCodec> #include <QIODevice> #include <QRegularExpression> #include <QCoreApplication> #include <QStringList> #include <BibTeXEntries> #include <BibTeXFields> #include <Preferences> #include <File> #include <Comment> #include <Macro> #include <Preamble> #include <Entry> #include <Element> #include <Value> #include "encoder.h" #include "encoderlatex.h" #include "logging_io.h" #define qint64toint(a) (static_cast<int>(qMax(0LL,qMin(0x7fffffffLL,(a))))) FileImporterBibTeX::FileImporterBibTeX(QObject *parent) - : FileImporter(parent), m_cancelFlag(false), m_textStream(nullptr), m_commentHandling(IgnoreComments), m_keywordCasing(KBibTeX::cLowerCase), m_lineNo(1) + : FileImporter(parent), m_cancelFlag(false), m_textStream(nullptr), m_commentHandling(CommentHandling::Ignore), m_keywordCasing(KBibTeX::Casing::LowerCase), m_lineNo(1) { m_keysForPersonDetection.append(Entry::ftAuthor); m_keysForPersonDetection.append(Entry::ftEditor); m_keysForPersonDetection.append(QStringLiteral("bookauthor")); /// used by JSTOR } File *FileImporterBibTeX::load(QIODevice *iodevice) { m_cancelFlag = false; if (!iodevice->isReadable() && !iodevice->open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Input device not readable"; - emit message(SeverityError, QStringLiteral("Input device not readable")); + emit message(MessageSeverity::Error, QStringLiteral("Input device not readable")); return nullptr; } File *result = new File(); /// Used to determine if file prefers quotation marks over /// curly brackets or the other way around m_statistics.countCurlyBrackets = 0; m_statistics.countQuotationMarks = 0; m_statistics.countFirstNameFirst = 0; m_statistics.countLastNameFirst = 0; m_statistics.countNoCommentQuote = 0; m_statistics.countCommentPercent = 0; m_statistics.countCommentCommand = 0; m_statistics.countProtectedTitle = 0; m_statistics.countUnprotectedTitle = 0; m_statistics.mostRecentListSeparator.clear(); m_textStream = new QTextStream(iodevice); m_textStream->setCodec(Preferences::defaultBibTeXEncoding.toLatin1()); ///< unless we learn something else, assume default codec result->setProperty(File::Encoding, Preferences::defaultBibTeXEncoding); QString rawText; rawText.reserve(qint64toint(iodevice->size())); while (!m_textStream->atEnd()) { QString line = m_textStream->readLine(); bool skipline = evaluateParameterComments(m_textStream, line.toLower(), result); // FIXME XML data should be removed somewhere else? onlinesearch ... if (line.startsWith(QStringLiteral("<?xml")) && line.endsWith(QStringLiteral("?>"))) /// Hop over XML declarations skipline = true; if (!skipline) rawText.append(line).append("\n"); } delete m_textStream; /** Remove HTML code from the input source */ // FIXME HTML data should be removed somewhere else? onlinesearch ... const int originalLength = rawText.length(); rawText = rawText.remove(KBibTeX::htmlRegExp); const int afterHTMLremovalLength = rawText.length(); if (originalLength != afterHTMLremovalLength) { qCInfo(LOG_KBIBTEX_IO) << (originalLength - afterHTMLremovalLength) << "characters of HTML tags have been removed"; - emit message(SeverityInfo, QString(QStringLiteral("%1 characters of HTML tags have been removed")).arg(originalLength - afterHTMLremovalLength)); + emit message(MessageSeverity::Info, QString(QStringLiteral("%1 characters of HTML tags have been removed")).arg(originalLength - afterHTMLremovalLength)); } // TODO really necessary to pipe data through several QTextStreams? m_textStream = new QTextStream(&rawText, QIODevice::ReadOnly); m_textStream->setCodec(Preferences::defaultBibTeXEncoding.toLower() == QStringLiteral("latex") ? "us-ascii" : Preferences::defaultBibTeXEncoding.toLatin1()); m_lineNo = 1; m_prevLine = m_currentLine = QString(); m_knownElementIds.clear(); readChar(); while (!m_nextChar.isNull() && !m_cancelFlag && !m_textStream->atEnd()) { emit progress(qint64toint(m_textStream->pos()), rawText.length()); Element *element = nextElement(); if (element != nullptr) { - if (m_commentHandling == KeepComments || !Comment::isComment(*element)) + if (m_commentHandling == CommentHandling::Keep || !Comment::isComment(*element)) result->append(QSharedPointer<Element>(element)); else delete element; } } emit progress(100, 100); if (m_cancelFlag) { qCWarning(LOG_KBIBTEX_IO) << "Loading bibliography data has been canceled"; - emit message(SeverityError, QStringLiteral("Loading bibliography data has been canceled")); + emit message(MessageSeverity::Error, QStringLiteral("Loading bibliography data has been canceled")); delete result; result = nullptr; } delete m_textStream; if (result != nullptr) { /// Set the file's preferences for string delimiters /// deduced from statistics built while parsing the file result->setProperty(File::StringDelimiter, m_statistics.countQuotationMarks > m_statistics.countCurlyBrackets ? QStringLiteral("\"\"") : QStringLiteral("{}")); /// Set the file's preferences for name formatting result->setProperty(File::NameFormatting, m_statistics.countFirstNameFirst > m_statistics.countLastNameFirst ? Preferences::personNameFormatFirstLast : Preferences::personNameFormatLastFirst); /// Set the file's preferences for title protected Qt::CheckState triState = (m_statistics.countProtectedTitle > m_statistics.countUnprotectedTitle * 4) ? Qt::Checked : ((m_statistics.countProtectedTitle * 4 < m_statistics.countUnprotectedTitle) ? Qt::Unchecked : Qt::PartiallyChecked); result->setProperty(File::ProtectCasing, static_cast<int>(triState)); /// Set the file's preferences for quoting of comments if (m_statistics.countNoCommentQuote > m_statistics.countCommentCommand && m_statistics.countNoCommentQuote > m_statistics.countCommentPercent) - result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcNone)); + result->setProperty(File::QuoteComment, static_cast<int>(Preferences::QuoteComment::None)); else if (m_statistics.countCommentCommand > m_statistics.countNoCommentQuote && m_statistics.countCommentCommand > m_statistics.countCommentPercent) - result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcCommand)); + result->setProperty(File::QuoteComment, static_cast<int>(Preferences::QuoteComment::Command)); else - result->setProperty(File::QuoteComment, static_cast<int>(Preferences::qcPercentSign)); + result->setProperty(File::QuoteComment, static_cast<int>(Preferences::QuoteComment::PercentSign)); if (!m_statistics.mostRecentListSeparator.isEmpty()) result->setProperty(File::ListSeparator, m_statistics.mostRecentListSeparator); // TODO gather more statistics for keyword casing etc. } iodevice->close(); return result; } bool FileImporterBibTeX::guessCanDecode(const QString &rawText) { static const QRegularExpression bibtexLikeText(QStringLiteral("@\\w+\\{.+\\}")); QString text = EncoderLaTeX::instance().decode(rawText); return bibtexLikeText.match(text).hasMatch(); } void FileImporterBibTeX::cancel() { m_cancelFlag = true; } Element *FileImporterBibTeX::nextElement() { Token token = nextToken(); - if (token == tAt) { + if (token == Token::At) { const QString elementType = readSimpleString(); const QString elementTypeLower = elementType.toLower(); if (elementTypeLower == QStringLiteral("comment")) { ++m_statistics.countCommentCommand; return readCommentElement(); } else if (elementTypeLower == QStringLiteral("string")) return readMacroElement(); else if (elementTypeLower == QStringLiteral("preamble")) return readPreambleElement(); else if (elementTypeLower == QStringLiteral("import")) { qCDebug(LOG_KBIBTEX_IO) << "Skipping potential HTML/JavaScript @import statement near line" << m_lineNo; - emit message(SeverityInfo, QString(QStringLiteral("Skipping potential HTML/JavaScript @import statement near line %1")).arg(m_lineNo)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Skipping potential HTML/JavaScript @import statement near line %1")).arg(m_lineNo)); return nullptr; } else if (!elementType.isEmpty()) return readEntryElement(elementType); else { qCWarning(LOG_KBIBTEX_IO) << "Element type after '@' is empty or invalid near line" << m_lineNo; - emit message(SeverityError, QString(QStringLiteral("Element type after '@' is empty or invalid near line %1")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Element type after '@' is empty or invalid near line %1")).arg(m_lineNo)); return nullptr; } - } else if (token == tUnknown && m_nextChar == QLatin1Char('%')) { + } else if (token == Token::Unknown && m_nextChar == QLatin1Char('%')) { /// do not complain about LaTeX-like comments, just eat them ++m_statistics.countCommentPercent; return readPlainCommentElement(QString()); - } else if (token == tUnknown) { + } else if (token == Token::Unknown) { if (m_nextChar.isLetter()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; - emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' near line %2, treating as comment")).arg(m_nextChar).arg(m_lineNo)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Unknown character '%1' near line %2, treating as comment")).arg(m_nextChar).arg(m_lineNo)); } else if (m_nextChar.isPrint()) { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ") near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; - emit message(SeverityInfo, QString(QStringLiteral("Unknown character '%1' (0x%2) near line %3, treating as comment")).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Unknown character '%1' (0x%2) near line %3, treating as comment")).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } else { qCDebug(LOG_KBIBTEX_IO) << "Unknown character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << ", treating as comment"; - emit message(SeverityInfo, QString(QStringLiteral("Unknown character 0x%1 near line %2, treating as comment")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Unknown character 0x%1 near line %2, treating as comment")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(m_lineNo)); } ++m_statistics.countNoCommentQuote; return readPlainCommentElement(QString(m_prevChar) + m_nextChar); } - if (token != tEOF) { + if (token != Token::EndOfFile) { qCWarning(LOG_KBIBTEX_IO) << "Don't know how to parse next token of type" << tokenidToString(token) << "in line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")" << endl; - emit message(SeverityError, QString(QStringLiteral("Don't know how to parse next token of type %1 in line %2")).arg(tokenidToString(token)).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Don't know how to parse next token of type %1 in line %2")).arg(tokenidToString(token)).arg(m_lineNo)); } return nullptr; } Comment *FileImporterBibTeX::readCommentElement() { if (!readCharUntil(QStringLiteral("{("))) return nullptr; return new Comment(EncoderLaTeX::instance().decode(readBracketString())); } Comment *FileImporterBibTeX::readPlainCommentElement(const QString &prefix) { QString result = EncoderLaTeX::instance().decode(prefix + readLine()); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); while (!m_nextChar.isNull() && m_nextChar != QLatin1Char('@')) { const QChar nextChar = m_nextChar; const QString line = readLine(); while (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) readChar(); result.append(EncoderLaTeX::instance().decode((nextChar == QLatin1Char('%') ? QString() : QString(nextChar)) + line)); } if (result.startsWith(QStringLiteral("x-kbibtex"))) { qCWarning(LOG_KBIBTEX_IO) << "Plain comment element starts with 'x-kbibtex', this should not happen"; - emit message(SeverityWarning, QStringLiteral("Plain comment element starts with 'x-kbibtex', this should not happen")); + emit message(MessageSeverity::Warning, QStringLiteral("Plain comment element starts with 'x-kbibtex', this should not happen")); /// ignore special comments return nullptr; } return new Comment(result); } Macro *FileImporterBibTeX::readMacroElement() { Token token = nextToken(); - while (token != tBracketOpen) { - if (token == tEOF) { + while (token != Token::BracketOpen) { + if (token == Token::EndOfFile) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing macro near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing macro near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString key = readSimpleString(); if (key.isEmpty()) { /// Cope with empty keys, /// duplicates are handled further below key = QStringLiteral("EmptyId"); } else if (!Encoder::containsOnlyAscii(key)) { /// Try to avoid non-ascii characters in ids const QString newKey = Encoder::instance().convertToPlainAscii(key); qCWarning(LOG_KBIBTEX_IO) << "Macro key" << key << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newKey; - emit message(SeverityWarning, QString(QStringLiteral("Macro key '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(key).arg(m_lineNo).arg(newKey)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Macro key '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(key).arg(m_lineNo).arg(newKey)); key = newKey; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(key)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newKey = newIdPattern.arg(key).arg(idx); while (m_knownElementIds.contains(newKey)) newKey = newIdPattern.arg(key).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate macro key" << key << ", using replacement key" << newKey; - emit message(SeverityWarning, QString(QStringLiteral("Duplicate macro key '%1', using replacement key '%2'")).arg(key, newKey)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Duplicate macro key '%1', using replacement key '%2'")).arg(key, newKey)); key = newKey; } m_knownElementIds.insert(key); - if (nextToken() != tAssign) { + if (nextToken() != Token::Assign) { qCCritical(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Assign symbol '=' expected")).arg(key).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Assign symbol '=' expected")).arg(key).arg(m_lineNo)); return nullptr; } Macro *macro = new Macro(key); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing macro" << key << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read macro's text"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Could not read macro's text")).arg(key).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing macro '%1' near line %2: Could not read macro's text")).arg(key).arg(m_lineNo)); delete macro; return nullptr; } text = EncoderLaTeX::instance().decode(bibtexAwareSimplify(text)); if (isStringKey) macro->value().append(QSharedPointer<MacroKey>(new MacroKey(text))); else macro->value().append(QSharedPointer<PlainText>(new PlainText(text))); token = nextToken(); - } while (token == tDoublecross); + } while (token == Token::Doublecross); return macro; } Preamble *FileImporterBibTeX::readPreambleElement() { Token token = nextToken(); - while (token != tBracketOpen) { - if (token == tEOF) { + while (token != Token::BracketOpen) { + if (token == Token::EndOfFile) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing preamble near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } Preamble *preamble = new Preamble(); do { bool isStringKey = false; QString text = readString(isStringKey); if (text.isNull()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing preamble near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Could not read preamble's text"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing preamble near line %1: Could not read preamble's text")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing preamble near line %1: Could not read preamble's text")).arg(m_lineNo)); delete preamble; return nullptr; } /// Remember: strings from preamble do not get encoded, /// may contain raw LaTeX commands and code text = bibtexAwareSimplify(text); if (isStringKey) preamble->value().append(QSharedPointer<MacroKey>(new MacroKey(text))); else preamble->value().append(QSharedPointer<PlainText>(new PlainText(text))); token = nextToken(); - } while (token == tDoublecross); + } while (token == Token::Doublecross); return preamble; } Entry *FileImporterBibTeX::readEntryElement(const QString &typeString) { Token token = nextToken(); - while (token != tBracketOpen) { - if (token == tEOF) { + while (token != Token::BracketOpen) { + if (token == Token::EndOfFile) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Opening curly brace '{' expected"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry near line %1: Opening curly brace '{' expected")).arg(m_lineNo)); return nullptr; } token = nextToken(); } QString id = readSimpleString(QStringLiteral(",}"), true).trimmed(); if (id.isEmpty()) { if (m_nextChar == QLatin1Char(',') || m_nextChar == QLatin1Char('}')) { /// Cope with empty ids, /// duplicates are handled further below id = QStringLiteral("EmptyId"); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine << "): Could not read entry id"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing preambentryle near line %1: Could not read entry id")).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing preambentryle near line %1: Could not read entry id")).arg(m_lineNo)); return nullptr; } } else { if (id.contains(QStringLiteral("\\")) || id.contains(QStringLiteral("{"))) { const QString newId = EncoderLaTeX::instance().decode(id); qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains backslashes or curly brackets, converted to" << newId; - emit message(SeverityWarning, QString(QStringLiteral("Entry id '%1' near line %2 contains backslashes or curly brackets, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Entry id '%1' near line %2 contains backslashes or curly brackets, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } if (!Encoder::containsOnlyAscii(id)) { /// Try to avoid non-ascii characters in ids const QString newId = Encoder::instance().convertToPlainAscii(id); qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newId; - emit message(SeverityWarning, QString(QStringLiteral("Entry id '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Entry id '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } } static const QVector<QChar> invalidIdCharacters = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char(',')}; for (const QChar &invalidIdCharacter : invalidIdCharacters) if (id.contains(invalidIdCharacter)) { qCWarning(LOG_KBIBTEX_IO) << "Entry id" << id << "near line" << m_lineNo << "contains invalid character" << invalidIdCharacter; - emit message(SeverityError, QString(QStringLiteral("Entry id '%1' near line %2 contains invalid character '%3'")).arg(id).arg(m_lineNo).arg(invalidIdCharacter)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Entry id '%1' near line %2 contains invalid character '%3'")).arg(id).arg(m_lineNo).arg(invalidIdCharacter)); return nullptr; } /// Check for duplicate entry ids, avoid collisions if (m_knownElementIds.contains(id)) { static const QString newIdPattern = QStringLiteral("%1-%2"); int idx = 2; QString newId = newIdPattern.arg(id).arg(idx); while (m_knownElementIds.contains(newId)) newId = newIdPattern.arg(id).arg(++idx); qCDebug(LOG_KBIBTEX_IO) << "Duplicate id" << id << "near line" << m_lineNo << ", using replacement id" << newId; - emit message(SeverityInfo, QString(QStringLiteral("Duplicate id '%1' near line %2, using replacement id '%3'")).arg(id).arg(m_lineNo).arg(newId)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Duplicate id '%1' near line %2, using replacement id '%3'")).arg(id).arg(m_lineNo).arg(newId)); id = newId; } m_knownElementIds.insert(id); Entry *entry = new Entry(BibTeXEntries::instance().format(typeString, m_keywordCasing), id); token = nextToken(); do { - if (token == tBracketClose) + if (token == Token::BracketClose) break; - else if (token == tEOF) { + else if (token == Token::EndOfFile) { qCWarning(LOG_KBIBTEX_IO) << "Unexpected end of data in entry" << id << "near line" << m_lineNo << ":" << m_prevLine << endl << m_currentLine; - emit message(SeverityError, QString(QStringLiteral("Unexpected end of data in entry '%1' near line %2")).arg(id).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Unexpected end of data in entry '%1' near line %2")).arg(id).arg(m_lineNo)); delete entry; return nullptr; - } else if (token != tComma) { + } else if (token != Token::Comma) { if (m_nextChar.isLetter()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(token" << tokenidToString(token) << ")"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(tokenidToString(token))); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(tokenidToString(token))); } else if (m_nextChar.isPrint()) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol ',' expected but got character" << m_nextChar << "(" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << ", token" << tokenidToString(token) << ")"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (0x%4, token %5)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character '%3' (0x%4, token %5)")).arg(id).arg(m_lineNo).arg(m_nextChar).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } else { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Comma symbol (,) expected but got character" << QString(QStringLiteral("0x%1")).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')) << "(token" << tokenidToString(token) << ")"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character 0x%3 (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Comma symbol ',' expected but got character 0x%3 (token %4)")).arg(id).arg(m_lineNo).arg(m_nextChar.unicode(), 4, 16, QLatin1Char('0')).arg(tokenidToString(token))); } delete entry; return nullptr; } QString keyName = BibTeXFields::instance().format(readSimpleString(), m_keywordCasing); if (keyName.isEmpty()) { token = nextToken(); - if (token == tBracketClose) { + if (token == Token::BracketClose) { /// Most often it is the case that the previous line ended with a comma, /// implying that this entry continues, but instead it gets closed by /// a closing curly bracket. qCDebug(LOG_KBIBTEX_IO) << "Issue while parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Last key-value pair ended with a non-conformant comma, ignoring that"; - emit message(SeverityInfo, QString(QStringLiteral("Issue while parsing entry '%1' near line %2: Last key-value pair ended with a non-conformant comma, ignoring that")).arg(id).arg(m_lineNo)); + emit message(MessageSeverity::Info, QString(QStringLiteral("Issue while parsing entry '%1' near line %2: Last key-value pair ended with a non-conformant comma, ignoring that")).arg(id).arg(m_lineNo)); break; } else { /// Something looks terribly wrong qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Closing curly bracket expected, but found" << tokenidToString(token); - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Closing curly bracket expected, but found %3")).arg(id).arg(m_lineNo).arg(tokenidToString(token))); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry '%1' near line %2: Closing curly bracket expected, but found %3")).arg(id).arg(m_lineNo).arg(tokenidToString(token))); delete entry; return nullptr; } } /// Try to avoid non-ascii characters in keys const QString newkeyName = Encoder::instance().convertToPlainAscii(keyName); if (newkeyName != keyName) { qCWarning(LOG_KBIBTEX_IO) << "Field name " << keyName << "near line" << m_lineNo << "contains non-ASCII characters, converted to" << newkeyName; - emit message(SeverityWarning, QString(QStringLiteral("Field name '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(keyName).arg(m_lineNo).arg(newkeyName)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Field name '%1' near line %2 contains non-ASCII characters, converted to '%3'")).arg(keyName).arg(m_lineNo).arg(newkeyName)); keyName = newkeyName; } token = nextToken(); - if (token != tAssign) { + if (token != Token::Assign) { qCWarning(LOG_KBIBTEX_IO) << "Error in parsing entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "): Assign symbol '=' expected after field name"; - emit message(SeverityError, QString(QStringLiteral("Error in parsing entry '%1', field name '%2' near line %3: Assign symbol '=' expected after field name")).arg(id, keyName).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Error in parsing entry '%1', field name '%2' near line %3: Assign symbol '=' expected after field name")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } Value value; /// check for duplicate fields if (entry->contains(keyName)) { if (keyName.toLower() == Entry::ftKeywords || keyName.toLower() == Entry::ftUrl) { /// Special handling of keywords and URLs: instead of using fallback names /// like "keywords2", "keywords3", ..., append new keywords to /// already existing keyword value value = entry->value(keyName); } else if (m_keysForPersonDetection.contains(keyName.toLower())) { /// Special handling of authors and editors: instead of using fallback names /// like "author2", "author3", ..., append new authors to /// already existing author value value = entry->value(keyName); } else { int i = 2; QString appendix = QString::number(i); while (entry->contains(keyName + appendix)) { ++i; appendix = QString::number(i); } qCDebug(LOG_KBIBTEX_IO) << "Entry" << id << "already contains a key" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << "), using" << (keyName + appendix); - emit message(SeverityWarning, QString(QStringLiteral("Entry '%1' already contains a key '%2' near line %4, using '%3'")).arg(id, keyName, keyName + appendix).arg(m_lineNo)); + emit message(MessageSeverity::Warning, QString(QStringLiteral("Entry '%1' already contains a key '%2' near line %4, using '%3'")).arg(id, keyName, keyName + appendix).arg(m_lineNo)); keyName += appendix; } } token = readValue(value, keyName); - if (token != tBracketClose && token != tComma) { + if (token != Token::BracketClose && token != Token::Comma) { qCWarning(LOG_KBIBTEX_IO) << "Failed to read value in entry" << id << ", field name" << keyName << "near line" << m_lineNo << "(" << m_prevLine << endl << m_currentLine << ")"; - emit message(SeverityError, QString(QStringLiteral("Failed to read value in entry '%1', field name '%2' near line %3")).arg(id, keyName).arg(m_lineNo)); + emit message(MessageSeverity::Error, QString(QStringLiteral("Failed to read value in entry '%1', field name '%2' near line %3")).arg(id, keyName).arg(m_lineNo)); delete entry; return nullptr; } entry->insert(keyName, value); } while (true); return entry; } FileImporterBibTeX::Token FileImporterBibTeX::nextToken() { if (!skipWhiteChar()) { /// Some error occurred while reading from data stream - return tEOF; + return Token::EndOfFile; } - Token result = tUnknown; + Token result = Token::Unknown; switch (m_nextChar.toLatin1()) { case '@': - result = tAt; + result = Token::At; break; case '{': case '(': - result = tBracketOpen; + result = Token::BracketOpen; break; case '}': case ')': - result = tBracketClose; + result = Token::BracketClose; break; case ',': - result = tComma; + result = Token::Comma; break; case '=': - result = tAssign; + result = Token::Assign; break; case '#': - result = tDoublecross; + result = Token::Doublecross; break; default: if (m_textStream->atEnd()) - result = tEOF; + result = Token::EndOfFile; } if (m_nextChar != QLatin1Char('%')) { /// Unclean solution, but necessary for comments /// that have a percent sign as a prefix readChar(); } return result; } QString FileImporterBibTeX::readString(bool &isStringKey) { /// Most often it is not a string key isStringKey = false; if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } switch (m_nextChar.toLatin1()) { case '{': case '(': { ++m_statistics.countCurlyBrackets; const QString result = readBracketString(); return result; } case '"': { ++m_statistics.countQuotationMarks; const QString result = readQuotedString(); return result; } default: isStringKey = true; const QString result = readSimpleString(); return result; } } QString FileImporterBibTeX::readSimpleString(const QString &until, const bool readNestedCurlyBrackets) { static const QString extraAlphaNumChars = QString(QStringLiteral("?'`-_:.+/$\\\"&")); QString result; ///< 'result' is Null on purpose: simple strings cannot be empty in contrast to e.g. quoted strings if (!skipWhiteChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } QChar prevChar = QChar(0x00); while (!m_nextChar.isNull()) { if (readNestedCurlyBrackets && m_nextChar == QLatin1Char('{') && prevChar != QLatin1Char('\\')) { int depth = 1; while (depth > 0) { result.append(m_nextChar); prevChar = m_nextChar; if (!readChar()) return result; if (m_nextChar == QLatin1Char('{') && prevChar != QLatin1Char('\\')) ++depth; else if (m_nextChar == QLatin1Char('}') && prevChar != QLatin1Char('\\')) --depth; } result.append(m_nextChar); prevChar = m_nextChar; if (!readChar()) return result; } const ushort nextCharUnicode = m_nextChar.unicode(); if (!until.isEmpty()) { /// Variable "until" has user-defined value if (m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r') || until.contains(m_nextChar)) { /// Force break on line-breaks or if one of the "until" chars has been read break; } else { /// Append read character to final result result.append(m_nextChar); } } else if ((nextCharUnicode >= static_cast<ushort>('a') && nextCharUnicode <= static_cast<ushort>('z')) || (nextCharUnicode >= static_cast<ushort>('A') && nextCharUnicode <= static_cast<ushort>('Z')) || (nextCharUnicode >= static_cast<ushort>('0') && nextCharUnicode <= static_cast<ushort>('9')) || extraAlphaNumChars.contains(m_nextChar)) { /// Accept default set of alpha-numeric characters result.append(m_nextChar); } else break; prevChar = m_nextChar; if (!readChar()) break; } return result; } QString FileImporterBibTeX::readQuotedString() { QString result(0, QChar()); ///< Construct an empty but non-null string Q_ASSERT_X(m_nextChar == QLatin1Char('"'), "QString FileImporterBibTeX::readQuotedString()", "m_nextChar is not '\"'"); if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } while (!m_nextChar.isNull()) { if (m_nextChar == QLatin1Char('"') && m_prevChar != QLatin1Char('\\') && m_prevChar != QLatin1Char('{')) break; else result.append(m_nextChar); if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } } if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } /// Remove protection around quotation marks result.replace(QStringLiteral("{\"}"), QStringLiteral("\"")); return result; } QString FileImporterBibTeX::readBracketString() { static const QChar backslash = QLatin1Char('\\'); QString result(0, QChar()); ///< Construct an empty but non-null string const QChar openingBracket = m_nextChar; const QChar closingBracket = openingBracket == QLatin1Char('{') ? QLatin1Char('}') : (openingBracket == QLatin1Char('(') ? QLatin1Char(')') : QChar()); Q_ASSERT_X(!closingBracket.isNull(), "QString FileImporterBibTeX::readBracketString()", "openingBracket==m_nextChar is neither '{' nor '('"); int counter = 1; if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } while (!m_nextChar.isNull()) { if (m_nextChar == openingBracket && m_prevChar != backslash) ++counter; else if (m_nextChar == closingBracket && m_prevChar != backslash) --counter; if (counter == 0) { break; } else result.append(m_nextChar); if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } } if (!readChar()) { /// Some error occurred while reading from data stream return QString(); ///< return null QString } return result; } FileImporterBibTeX::Token FileImporterBibTeX::readValue(Value &value, const QString &key) { - Token token = tUnknown; + Token token = Token::Unknown; const QString iKey = key.toLower(); static const QSet<QString> verbatimKeys {Entry::ftColor.toLower(), Entry::ftCrossRef.toLower(), Entry::ftXData.toLower()}; do { bool isStringKey = false; const QString rawText = readString(isStringKey); if (rawText.isNull()) - return tEOF; + return Token::EndOfFile; QString text = EncoderLaTeX::instance().decode(rawText); /// for all entries except for abstracts ... if (iKey != Entry::ftAbstract && !(iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) && !iKey.startsWith(Entry::ftLocalFile) && !iKey.startsWith(Entry::ftFile)) { /// ... remove redundant spaces including newlines text = bibtexAwareSimplify(text); } /// abstracts will keep their formatting (regarding line breaks) /// as requested by Thomas Jensch via mail (20 October 2010) /// Maintain statistics on if (book) titles are protected /// by surrounding curly brackets if (iKey == Entry::ftTitle || iKey == Entry::ftBookTitle) { if (text[0] == QLatin1Char('{') && text[text.length() - 1] == QLatin1Char('}')) ++m_statistics.countProtectedTitle; else ++m_statistics.countUnprotectedTitle; } if (m_keysForPersonDetection.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { - CommaContainment comma = ccContainsComma; + CommaContainment comma = CommaContainment::Contains; parsePersonList(text, value, &comma, m_lineNo, this); /// Update statistics on name formatting - if (comma == ccContainsComma) + if (comma == CommaContainment::Contains) ++m_statistics.countLastNameFirst; else ++m_statistics.countFirstNameFirst; } } else if (iKey == Entry::ftPages) { static const QRegularExpression rangeInAscii(QStringLiteral("\\s*--?\\s*")); text.replace(rangeInAscii, QChar(0x2013)); if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if ((iKey.startsWith(Entry::ftUrl) && !iKey.startsWith(Entry::ftUrlDate)) || iKey.startsWith(Entry::ftLocalFile) || iKey.startsWith(Entry::ftFile) || iKey == QStringLiteral("ee") || iKey == QStringLiteral("biburl")) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: in fields like Url or LocalFile, file names are separated by ; static const QRegularExpression semicolonSpace = QRegularExpression(QStringLiteral("[;]\\s*")); const QStringList fileList = rawText.split(semicolonSpace, QString::SkipEmptyParts); for (const QString &filename : fileList) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } } } else if (iKey.startsWith(Entry::ftFile)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Assumption: this field was written by Mendeley, which uses /// a very strange format for file names: /// :C$\backslash$:/Users/BarisEvrim/Documents/Mendeley Desktop/GeversPAMI10.pdf:pdf /// :: /// :Users/Fred/Library/Application Support/Mendeley Desktop/Downloaded/Hasselman et al. - 2011 - (Still) Growing Up What should we be a realist about in the cognitive and behavioural sciences Abstract.pdf:pdf const QRegularExpressionMatch match = KBibTeX::mendeleyFileRegExp.match(rawText); if (match.hasMatch()) { static const QString backslashLaTeX = QStringLiteral("$\\backslash$"); QString filename = match.captured(1).remove(backslashLaTeX); if (filename.startsWith(QStringLiteral("home/")) || filename.startsWith(QStringLiteral("Users/"))) { /// Mendeley doesn't have a slash at the beginning of absolute paths, /// so, insert one /// See bug 19833, comment 5: https://gna.org/bugs/index.php?19833#comment5 filename.prepend(QLatin1Char('/')); } value.append(QSharedPointer<VerbatimText>(new VerbatimText(filename))); } else value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); } } else if (iKey == Entry::ftMonth) { if (isStringKey) { static const QRegularExpression monthThreeChars(QStringLiteral("^[a-z]{3}"), QRegularExpression::CaseInsensitiveOption); if (monthThreeChars.match(text).hasMatch()) text = text.left(3).toLower(); value.append(QSharedPointer<MacroKey>(new MacroKey(text))); } else value.append(QSharedPointer<PlainText>(new PlainText(text))); } else if (iKey.startsWith(Entry::ftDOI)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { /// Take care of "; " which separates multiple DOIs, but which may baffle the regexp QString preprocessedText = rawText; preprocessedText.replace(QStringLiteral("; "), QStringLiteral(" ")); /// Extract everything that looks like a DOI using a regular expression, /// ignore everything else QRegularExpressionMatchIterator doiRegExpMatchIt = KBibTeX::doiRegExp.globalMatch(preprocessedText); while (doiRegExpMatchIt.hasNext()) { const QRegularExpressionMatch doiRegExpMatch = doiRegExpMatchIt.next(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(doiRegExpMatch.captured(0)))); } } } else if (iKey == Entry::ftKeywords) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { char splitChar; const QList<QSharedPointer<Keyword> > keywords = splitKeywords(text, &splitChar); for (const auto &keyword : keywords) value.append(keyword); /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (splitChar == ';') m_statistics.mostRecentListSeparator = QStringLiteral("; "); else if (splitChar == ',') m_statistics.mostRecentListSeparator = QStringLiteral(", "); } } else if (verbatimKeys.contains(iKey)) { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); } else { if (isStringKey) value.append(QSharedPointer<MacroKey>(new MacroKey(text))); else value.append(QSharedPointer<PlainText>(new PlainText(text))); } token = nextToken(); - } while (token == tDoublecross); + } while (token == Token::Doublecross); return token; } bool FileImporterBibTeX::readChar() { /// Memorize previous char m_prevChar = m_nextChar; if (m_textStream->atEnd()) { /// At end of data stream m_nextChar = QChar::Null; return false; } /// Read next char *m_textStream >> m_nextChar; /// Test for new line if (m_nextChar == QLatin1Char('\n')) { /// Update variables tracking line numbers and line content ++m_lineNo; m_prevLine = m_currentLine; m_currentLine.clear(); } else { /// Add read char to current line m_currentLine.append(m_nextChar); } return true; } bool FileImporterBibTeX::readCharUntil(const QString &until) { Q_ASSERT_X(!until.isEmpty(), "bool FileImporterBibTeX::readCharUntil(const QString &until)", "\"until\" is empty or invalid"); bool result = true; while (!until.contains(m_nextChar) && (result = readChar())); return result; } bool FileImporterBibTeX::skipWhiteChar() { bool result = true; while ((m_nextChar.isSpace() || m_nextChar == QLatin1Char('\t') || m_nextChar == QLatin1Char('\n') || m_nextChar == QLatin1Char('\r')) && result) result = readChar(); return result; } QString FileImporterBibTeX::readLine() { QString result; while (m_nextChar != QLatin1Char('\n') && m_nextChar != QLatin1Char('\r') && readChar()) result.append(m_nextChar); return result; } QList<QSharedPointer<Keyword> > FileImporterBibTeX::splitKeywords(const QString &text, char *usedSplitChar) { QList<QSharedPointer<Keyword> > result; static const QHash<char, QRegularExpression> splitAlong = { {'\n', QRegularExpression(QStringLiteral("\\s*\n\\s*"))}, {';', QRegularExpression(QStringLiteral("\\s*;\\s*"))}, {',', QRegularExpression(QString("\\s*,\\s*"))} }; if (usedSplitChar != nullptr) *usedSplitChar = '\0'; for (auto it = splitAlong.constBegin(); it != splitAlong.constEnd(); ++it) { /// check if character is contained in text (should be cheap to test) if (text.contains(QLatin1Char(it.key()))) { /// split text along a pattern like spaces-splitchar-spaces /// extract keywords static const QRegularExpression unneccessarySpacing(QStringLiteral("[ \n\r\t]+")); const QStringList keywords = text.split(it.value(), QString::SkipEmptyParts).replaceInStrings(unneccessarySpacing, QStringLiteral(" ")); /// build QList of Keyword objects from keywords for (const QString &keyword : keywords) { result.append(QSharedPointer<Keyword>(new Keyword(keyword))); } /// Memorize (some) split characters for later use /// (e.g. when writing file again) if (usedSplitChar != nullptr) *usedSplitChar = it.key(); /// no more splits necessary break; } } /// no split was performed, so whole text must be a single keyword if (result.isEmpty()) result.append(QSharedPointer<Keyword>(new Keyword(text))); return result; } QList<QSharedPointer<Person> > FileImporterBibTeX::splitNames(const QString &text, const int line_number, QObject *parent) { /// Case: Smith, John and Johnson, Tim /// Case: Smith, John and Fulkerson, Ford and Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, and Johnson, Tim /// Case: John Smith and Tim Johnson /// Case: John Smith and Ford Fulkerson and Tim Johnson /// Case: Smith, John, Johnson, Tim /// Case: Smith, John, Fulkerson, Ford, Johnson, Tim /// Case: John Smith, Tim Johnson /// Case: John Smith, Tim Johnson, Ford Fulkerson /// Case: Smith, John ; Johnson, Tim ; Fulkerson, Ford (IEEE Xplore) /// German case: Robert A. Gehring und Bernd Lutterbeck QString internalText = text; /// Remove invalid characters such as dots or (double) daggers for footnotes static const QList<QChar> invalidChars {QChar(0x00b7), QChar(0x2020), QChar(0x2217), QChar(0x2021), QChar(0x002a), QChar(0x21d1) /** Upwards double arrow */}; for (const auto &invalidChar : invalidChars) /// Replacing daggers with commas ensures that they act as persons' names separator internalText = internalText.replace(invalidChar, QChar(',')); /// Remove numbers to footnotes static const QRegularExpression numberFootnoteRegExp(QStringLiteral("(\\w)\\d+\\b")); internalText = internalText.replace(numberFootnoteRegExp, QStringLiteral("\\1")); /// Remove academic degrees static const QRegularExpression academicDegreesRegExp(QStringLiteral("(,\\s*)?(MA|PhD)\\b")); internalText = internalText.remove(academicDegreesRegExp); /// Remove email addresses static const QRegularExpression emailAddressRegExp(QStringLiteral("\\b[a-zA-Z0-9][a-zA-Z0-9._-]+[a-zA-Z0-9]@[a-z0-9][a-z0-9-]*([.][a-z0-9-]+)*([.][a-z]+)+\\b")); internalText = internalText.remove(emailAddressRegExp); /// Split input string into tokens which are either name components (first or last name) /// or full names (composed of first and last name), depending on the input string's structure static const QRegularExpression split(QStringLiteral("\\s*([,]+|[,]*\\b[au]nd\\b|[;]|&|\\n|\\s{4,})\\s*")); const QStringList authorTokenList = internalText.split(split, QString::SkipEmptyParts); bool containsSpace = true; for (QStringList::ConstIterator it = authorTokenList.constBegin(); containsSpace && it != authorTokenList.constEnd(); ++it) containsSpace = (*it).contains(QChar(' ')); QList<QSharedPointer<Person> > result; result.reserve(authorTokenList.size()); if (containsSpace) { /// Tokens look like "John Smith" for (const QString &authorToken : authorTokenList) { QSharedPointer<Person> person = personFromString(authorToken, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } } else { /// Tokens look like "Smith" or "John" /// Assumption: two consecutive tokens form a name for (QStringList::ConstIterator it = authorTokenList.constBegin(); it != authorTokenList.constEnd(); ++it) { QString lastname = *it; ++it; if (it != authorTokenList.constEnd()) { lastname += QStringLiteral(", ") + (*it); QSharedPointer<Person> person = personFromString(lastname, nullptr, line_number, parent); if (!person.isNull()) result.append(person); } else break; } } return result; } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, const int line_number, QObject *parent) { parsePersonList(text, value, nullptr, line_number, parent); } void FileImporterBibTeX::parsePersonList(const QString &text, Value &value, CommaContainment *comma, const int line_number, QObject *parent) { static const QString tokenAnd = QStringLiteral("and"); static const QString tokenOthers = QStringLiteral("others"); static QStringList tokens; contextSensitiveSplit(text, tokens); if (tokens.count() > 0) { if (tokens[0] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenAnd << "near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'and' near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'and' near line %1")).arg(line_number))); } else if (tokens.count() > 1 && tokens[tokens.count() - 1] == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenAnd << "near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'and' near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'and' near line %1")).arg(line_number))); } if (tokens[0] == tokenOthers) { qCInfo(LOG_KBIBTEX_IO) << "Person list starts with" << tokenOthers << "near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'others' near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Person list starts with 'others' near line %1")).arg(line_number))); } else if (tokens[tokens.count() - 1] == tokenOthers && (tokens.count() < 3 || tokens[tokens.count() - 2] != tokenAnd)) { qCInfo(LOG_KBIBTEX_IO) << "Person list ends with" << tokenOthers << "but is not preceeded with name and" << tokenAnd << "near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'others' but is not preceeded with name and 'and' near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Person list ends with 'others' but is not preceeded with name and 'and' near line %1")).arg(line_number))); } } int nameStart = 0; QString prevToken; for (int i = 0; i < tokens.count(); ++i) { if (tokens[i] == tokenAnd) { if (prevToken == tokenAnd) { qCInfo(LOG_KBIBTEX_IO) << "Two subsequent" << tokenAnd << "found in person list near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Two subsequent 'and' found in person list near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Two subsequent 'and' found in person list near line %1")).arg(line_number))); } else if (nameStart < i) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart, i - nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart, i - nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart, i - nameStart).join(' ')).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart, i - nameStart).join(' ')).arg(line_number))); } } else { qCInfo(LOG_KBIBTEX_IO) << "Found" << tokenAnd << "but no name before it near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Found 'and' but no name before it near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Found 'and' but no name before it near line %1")).arg(line_number))); } nameStart = i + 1; } else if (tokens[i] == tokenOthers) { if (i < tokens.count() - 1) { qCInfo(LOG_KBIBTEX_IO) << "Special word" << tokenOthers << "found before last position in person name near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Special word 'others' found before last position in person name near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Special word 'others' found before last position in person name near line %1")).arg(line_number))); } else value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others")))); nameStart = tokens.count() + 1; } prevToken = tokens[i]; } if (nameStart < tokens.count()) { const QSharedPointer<Person> person = personFromTokenList(tokens.mid(nameStart), comma, line_number, parent); if (!person.isNull()) value.append(person); else { qCInfo(LOG_KBIBTEX_IO) << "Text" << tokens.mid(nameStart).join(' ') << "does not form a name near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart).join(' ')).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Text '%1' does not form a name near line %2")).arg(tokens.mid(nameStart).join(' ')).arg(line_number))); } } } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, const int line_number, QObject *parent) { // TODO Merge with FileImporter::splitName return personFromString(name, nullptr, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromString(const QString &name, CommaContainment *comma, const int line_number, QObject *parent) { // TODO Merge with FileImporter::splitName and FileImporterBibTeX::contextSensitiveSplit static QStringList tokens; contextSensitiveSplit(name, tokens); return personFromTokenList(tokens, comma, line_number, parent); } QSharedPointer<Person> FileImporterBibTeX::personFromTokenList(const QStringList &tokens, CommaContainment *comma, const int line_number, QObject *parent) { - if (comma != nullptr) *comma = ccNoComma; + if (comma != nullptr) *comma = CommaContainment::None; /// Simple case: provided list of tokens is empty, return invalid Person if (tokens.isEmpty()) return QSharedPointer<Person>(); /** * Sequence of tokens may contain somewhere a comma, like * "Tuckwell," "Peter". In this case, fill two string lists: * one with tokens before the comma, one with tokens after the * comma (excluding the comma itself). Example: * partA = ( "Tuckwell" ); partB = ( "Peter" ); partC = ( "Jr." ) * If a comma was found, boolean variable gotComma is set. */ QStringList partA, partB, partC; int commaCount = 0; for (const QString &token : tokens) { /// Position where comma was found, or -1 if no comma in token int p = -1; if (commaCount < 2) { /// Only check if token contains comma /// if no comma was found before int bracketCounter = 0; for (int i = 0; i < token.length(); ++i) { /// Consider opening curly brackets if (token[i] == QChar('{')) ++bracketCounter; /// Consider closing curly brackets else if (token[i] == QChar('}')) --bracketCounter; /// Only if outside any open curly bracket environments /// consider comma characters else if (bracketCounter == 0 && token[i] == QChar(',')) { /// Memorize comma's position and break from loop p = i; break; } else if (bracketCounter < 0) { /// Should never happen: more closing brackets than opening ones qCWarning(LOG_KBIBTEX_IO) << "Opening and closing brackets do not match near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Opening and closing brackets do not match near line %1")).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Opening and closing brackets do not match near line %1")).arg(line_number))); } } } if (p >= 0) { if (commaCount == 0) { if (p > 0) partA.append(token.left(p)); if (p < token.length() - 1) partB.append(token.mid(p + 1)); } else if (commaCount == 1) { if (p > 0) partB.append(token.left(p)); if (p < token.length() - 1) partC.append(token.mid(p + 1)); } ++commaCount; } else if (commaCount == 0) partA.append(token); else if (commaCount == 1) partB.append(token); else if (commaCount == 2) partC.append(token); } if (commaCount > 0) { - if (comma != nullptr) *comma = ccContainsComma; + if (comma != nullptr) *comma = CommaContainment::Contains; return QSharedPointer<Person>(new Person(partC.isEmpty() ? partB.join(QChar(' ')) : partC.join(QChar(' ')), partA.join(QChar(' ')), partC.isEmpty() ? QString() : partB.join(QChar(' ')))); } /** * PubMed uses a special writing style for names, where the * last name is followed by single capital letters, each being * the first letter of each first name. Example: Tuckwell P H * So, check how many single capital letters are at the end of * the given token list */ partA.clear(); partB.clear(); bool singleCapitalLetters = true; QStringList::ConstIterator it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (singleCapitalLetters && it->length() == 1 && it->at(0).isUpper()) partB.prepend(*it); else { singleCapitalLetters = false; partA.prepend(*it); } } if (!partB.isEmpty()) { /// Name was actually given in PubMed format return QSharedPointer<Person>(new Person(partB.join(QChar(' ')), partA.join(QChar(' ')))); } /** * Normally, the last upper case token in a name is the last name * (last names consisting of multiple space-separated parts *have* * to be protected by {...}), but some languages have fill words * in lower case belonging to the last name as well (example: "van"). * In addition, some languages have capital case letters as well * (example: "Di Cosmo"). * Exception: Special keywords such as "Jr." can be appended to the * name, not counted as part of the last name. */ partA.clear(); partB.clear(); partC.clear(); static const QSet<QString> capitalCaseLastNameFragments {QStringLiteral("Di")}; it = tokens.constEnd(); while (it != tokens.constBegin()) { --it; if (partB.isEmpty() && (it->toLower().startsWith(QStringLiteral("jr")) || it->toLower().startsWith(QStringLiteral("sr")) || it->toLower().startsWith(QStringLiteral("iii")))) /// handle name suffices like "Jr" or "III." partC.prepend(*it); else if (partB.isEmpty() || it->at(0).isLower() || capitalCaseLastNameFragments.contains(*it)) partB.prepend(*it); else partA.prepend(*it); } if (!partB.isEmpty()) { /// Name was actually like "Peter Ole van der Tuckwell", /// split into "Peter Ole" and "van der Tuckwell" return QSharedPointer<Person>(new Person(partA.join(QChar(' ')), partB.join(QChar(' ')), partC.isEmpty() ? QString() : partC.join(QChar(' ')))); } qCWarning(LOG_KBIBTEX_IO) << "Don't know how to handle name" << tokens.join(QLatin1Char(' ')) << "near line" << line_number; if (parent != nullptr) - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Don't know how to handle name '%1' near line %2")).arg(tokens.join(QLatin1Char(' '))).arg(line_number))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Don't know how to handle name '%1' near line %2")).arg(tokens.join(QLatin1Char(' '))).arg(line_number))); return QSharedPointer<Person>(); } void FileImporterBibTeX::contextSensitiveSplit(const QString &text, QStringList &segments) { // TODO Merge with FileImporter::splitName and FileImporterBibTeX::personFromString int bracketCounter = 0; ///< keep track of opening and closing brackets: {...} QString buffer; int len = text.length(); segments.clear(); ///< empty list for results before proceeding for (int pos = 0; pos < len; ++pos) { if (text[pos] == '{') ++bracketCounter; else if (text[pos] == '}') --bracketCounter; if (text[pos].isSpace() && bracketCounter == 0) { if (!buffer.isEmpty()) { segments.append(buffer); buffer.clear(); } } else buffer.append(text[pos]); } if (!buffer.isEmpty()) segments.append(buffer); } QString FileImporterBibTeX::bibtexAwareSimplify(const QString &text) { QString result; int i = 0; /// Consume initial spaces ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but if there have been spaces (i.e. i>0), then record a single space only if (i > 0) result.append(QStringLiteral(" ")); while (i < text.length()) { /// Consume non-spaces while (i < text.length() && !text[i].isSpace()) { result.append(text[i]); ++i; } /// String may end with a non-space if (i >= text.length()) break; /// Consume spaces, ... while (i < text.length() && text[i].isSpace()) ++i; /// ... but record only a single space result.append(QStringLiteral(" ")); } return result; } bool FileImporterBibTeX::evaluateParameterComments(QTextStream *textStream, const QString &line, File *file) { /// Assertion: variable "line" is all lower-case /** check if this file requests a special encoding */ if (line.startsWith(QStringLiteral("@comment{x-kbibtex-encoding=")) && line.endsWith(QLatin1Char('}'))) { const QString encoding = line.mid(28, line.length() - 29).toLower(); textStream->setCodec(encoding.toLower() == QStringLiteral("latex") ? "us-ascii" : encoding.toLatin1()); file->setProperty(File::Encoding, encoding.toLower() == QStringLiteral("latex") ? encoding : QString::fromLatin1(textStream->codec()->name())); return true; } else if (line.startsWith(QStringLiteral("@comment{x-kbibtex-personnameformatting=")) && line.endsWith(QLatin1Char('}'))) { // TODO usage of x-kbibtex-personnameformatting is deprecated, // as automatic detection is in place QString personNameFormatting = line.mid(40, line.length() - 41); file->setProperty(File::NameFormatting, personNameFormatting); return true; } else if (line.startsWith(QStringLiteral("% encoding:"))) { /// Interprete JabRef's encoding information QString encoding = line.mid(12); qCDebug(LOG_KBIBTEX_IO) << "Using JabRef's encoding:" << encoding; textStream->setCodec(encoding.toLatin1()); file->setProperty(File::Encoding, QString::fromLatin1(textStream->codec()->name())); return true; } return false; } QString FileImporterBibTeX::tokenidToString(Token token) { switch (token) { - case tAt: return QString(QStringLiteral("At")); - case tBracketClose: return QString(QStringLiteral("BracketClose")); - case tBracketOpen: return QString(QStringLiteral("BracketOpen")); - case tAlphaNumText: return QString(QStringLiteral("AlphaNumText")); - case tAssign: return QString(QStringLiteral("Assign")); - case tComma: return QString(QStringLiteral("Comma")); - case tDoublecross: return QString(QStringLiteral("Doublecross")); - case tEOF: return QString(QStringLiteral("EOF")); - case tUnknown: return QString(QStringLiteral("Unknown")); + case Token::At: return QString(QStringLiteral("At")); + case Token::BracketClose: return QString(QStringLiteral("BracketClose")); + case Token::BracketOpen: return QString(QStringLiteral("BracketOpen")); + case Token::AlphaNumText: return QString(QStringLiteral("AlphaNumText")); + case Token::Assign: return QString(QStringLiteral("Assign")); + case Token::Comma: return QString(QStringLiteral("Comma")); + case Token::Doublecross: return QString(QStringLiteral("Doublecross")); + case Token::EndOfFile: return QString(QStringLiteral("EOF")); + case Token::Unknown: return QString(QStringLiteral("Unknown")); default: return QString(QStringLiteral("<Unknown>")); } } void FileImporterBibTeX::setCommentHandling(CommentHandling commentHandling) { m_commentHandling = commentHandling; } diff --git a/src/io/fileimporterbibtex.h b/src/io/fileimporterbibtex.h index 961b5fbb..91cd0f96 100644 --- a/src/io/fileimporterbibtex.h +++ b/src/io/fileimporterbibtex.h @@ -1,171 +1,171 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEIMPORTERBIBTEX_H #define KBIBTEX_IO_FILEIMPORTERBIBTEX_H #include <QTextStream> #include <QSharedPointer> #include <QStringList> #include <QSet> #include <KBibTeX> #include <FileImporter> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 class Element; class Comment; class Preamble; class Macro; class Entry; class Value; class Keyword; /** * This class reads a BibTeX file from a QIODevice (such as a QFile) and * creates a File object which can be used to access the BibTeX elements. * @see File * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXIO_EXPORT FileImporterBibTeX : public FileImporter { Q_OBJECT public: - enum CommentHandling {IgnoreComments = 0, KeepComments = 1}; + enum class CommentHandling {Ignore, Keep}; /** * Creates an importer class to read a BibTeX file. */ explicit FileImporterBibTeX(QObject *parent); /** * Read data from the given device and construct a File object holding * the bibliographic data. * @param iodevice opened QIODevice instance ready to read from * @return @c valid File object with elements, @c NULL if reading failed for some reason */ File *load(QIODevice *iodevice) override; /** TODO */ static bool guessCanDecode(const QString &text); /** * Split a list of keyword separated by ";" or "," into single Keyword objects. * @param text Text containing the keyword list * @return A list of Keyword object containing the keywords * @see Keyword */ static QList<QSharedPointer<Keyword> > splitKeywords(const QString &text, char *usedSplitChar = nullptr); /** * Split a list of names into single Person objects. * Examples: "Smith, John, Fulkerson, Ford, and Johnson, Tim" * or "John Smith and Tim Johnson" * @param text Text containing the persons' names * @return A list of Person object containing the names * @see Person */ static QList<QSharedPointer<Person> > splitNames(const QString &text, const int line_number = 1, QObject *parent = nullptr); /** * Split a person's name into its parts and construct a Person object from them. * This is a functions specialized on the properties of (La)TeX code considering * e.g. curly brackets. * @param name The persons name * @return A Person object containing the name * @see Person */ static QSharedPointer<Person> personFromString(const QString &name, const int line_number = 1, QObject *parent = nullptr); static void parsePersonList(const QString &text, Value &value, const int line_number = 1, QObject *parent = nullptr); void setCommentHandling(CommentHandling commentHandling); public slots: void cancel() override; private: - enum Token { - tAt = 1, tBracketOpen = 2, tBracketClose = 3, tAlphaNumText = 4, tComma = 5, tAssign = 6, tDoublecross = 7, tEOF = 0xffff, tUnknown = -1 + enum class Token { + At = 1, BracketOpen = 2, BracketClose = 3, AlphaNumText = 4, Comma = 5, Assign = 6, Doublecross = 7, EndOfFile = 0xffff, Unknown = -1 }; - enum CommaContainment { ccNoComma = 0, ccContainsComma = 1 }; + enum class CommaContainment { None, Contains }; struct { int countCurlyBrackets, countQuotationMarks; int countFirstNameFirst, countLastNameFirst; int countNoCommentQuote, countCommentPercent, countCommentCommand; int countProtectedTitle, countUnprotectedTitle; QString mostRecentListSeparator; } m_statistics; bool m_cancelFlag; QTextStream *m_textStream; CommentHandling m_commentHandling; KBibTeX::Casing m_keywordCasing; QStringList m_keysForPersonDetection; QSet<QString> m_knownElementIds; /// low-level character operations QChar m_prevChar, m_nextChar; - unsigned int m_lineNo; + int m_lineNo; QString m_prevLine, m_currentLine; bool readChar(); bool readCharUntil(const QString &until); bool skipWhiteChar(); QString readLine(); /// high-level parsing functions Comment *readCommentElement(); Comment *readPlainCommentElement(const QString &prefix); Macro *readMacroElement(); Preamble *readPreambleElement(); Entry *readEntryElement(const QString &typeString); Element *nextElement(); Token nextToken(); QString readString(bool &isStringKey); QString readSimpleString(const QString &until = QString(), const bool readNestedCurlyBrackets = false); QString readQuotedString(); QString readBracketString(); Token readValue(Value &value, const QString &fieldType); static QSharedPointer<Person> personFromString(const QString &name, CommaContainment *comma, const int line_number, QObject *parent); static QSharedPointer<Person> personFromTokenList(const QStringList &tokens, CommaContainment *comma, const int line_number, QObject *parent); static void parsePersonList(const QString &text, Value &value, CommaContainment *comma, const int line_number, QObject *parent); /** * Split a string into white-space separated chunks, * but keep parts intact which are protected by {...}. * Example: "aa bb ccc {dd ee ff}" * will be split into "aa", "bb", "ccc", "{dd ee ff}" * * @param text input string to be split * @param segments list where chunks will be added to */ static void contextSensitiveSplit(const QString &text, QStringList &segments); static QString bibtexAwareSimplify(const QString &text); bool evaluateParameterComments(QTextStream *textStream, const QString &line, File *file); QString tokenidToString(Token token); }; #endif // KBIBTEX_IO_FILEIMPORTERBIBTEX_H diff --git a/src/io/fileimporterbibutils.cpp b/src/io/fileimporterbibutils.cpp index 3661c187..1795f699 100644 --- a/src/io/fileimporterbibutils.cpp +++ b/src/io/fileimporterbibutils.cpp @@ -1,71 +1,71 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileimporterbibutils.h" #include <QBuffer> #include "fileimporterbibtex.h" #include "logging_io.h" class FileImporterBibUtils::Private { private: // UNUSED FileImporterBibUtils *p; public: FileImporterBibTeX *bibtexImporter; Private(FileImporterBibUtils *parent) // UNUSED : p(parent) { bibtexImporter = new FileImporterBibTeX(parent); connect(bibtexImporter, &FileImporterBibTeX::message, parent, &FileImporterBibUtils::message); } ~Private() { delete bibtexImporter; } }; FileImporterBibUtils::FileImporterBibUtils(QObject *parent) : FileImporter(parent), BibUtils(), d(new FileImporterBibUtils::Private(this)) { /// nothing } FileImporterBibUtils::~FileImporterBibUtils() { delete d; } File *FileImporterBibUtils::load(QIODevice *iodevice) { if (!iodevice->isReadable() && !iodevice->open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Input device not readable"; return nullptr; } QBuffer buffer; - const bool result = convert(*iodevice, format(), buffer, BibUtils::BibTeX); + const bool result = convert(*iodevice, format(), buffer, BibUtils::Format::BibTeX); iodevice->close(); if (result) return d->bibtexImporter->load(&buffer); else return nullptr; } diff --git a/src/io/fileimporterris.cpp b/src/io/fileimporterris.cpp index 27c494c8..0fa582a7 100644 --- a/src/io/fileimporterris.cpp +++ b/src/io/fileimporterris.cpp @@ -1,340 +1,340 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileimporterris.h" #include <QVector> #include <QTextStream> #include <QRegularExpression> #include <QCoreApplication> #include <QStringList> #include <Preferences> #include <KBibTeX> #include <Entry> #include <Value> #include "logging_io.h" #define appendValue(entry, fieldname, newvalue) { Value value = (entry)->value((fieldname)); value.append((newvalue)); (entry)->insert((fieldname), value); } #define removeDuplicates(entry, fieldname) { Value value = (entry)->value((fieldname)); if (!(value).isEmpty()) removeDuplicateValueItems((value)); if (!(value).isEmpty()) (entry)->insert((fieldname), value); } class FileImporterRIS::FileImporterRISPrivate { public: FileImporterRIS *parent; int referenceCounter; bool cancelFlag; bool protectCasing; typedef struct { QString key; QString value; } RISitem; typedef QVector<RISitem> RISitemList; FileImporterRISPrivate(FileImporterRIS *_parent) : parent(_parent), referenceCounter(0), cancelFlag(false), protectCasing(false) { /// nothing } RISitemList readElement(QTextStream &textStream) { RISitemList result; QString line = textStream.readLine(); while (!line.startsWith(QStringLiteral("TY - ")) && !textStream.atEnd()) line = textStream.readLine(); if (textStream.atEnd()) return result; QString key, value; while (!line.startsWith(QStringLiteral("ER -")) && !textStream.atEnd()) { if (line.mid(2, 3) == QStringLiteral(" -")) { if (!value.isEmpty()) { RISitem item; item.key = key; item.value = value; result.append(item); } key = line.left(2); value = line.mid(6).simplified(); } else { line = line.simplified(); if (line.length() > 1) { /// multi-line field are joined to one long line value += QLatin1Char(' ') + line; } } line = textStream.readLine(); } if (!line.startsWith(QStringLiteral("ER -")) && textStream.atEnd()) { qCWarning(LOG_KBIBTEX_IO) << "Expected that entry that starts with 'TY' ends with 'ER' but instead met end of file"; /// Instead of an 'emit' ... - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QStringLiteral("Expected that entry that starts with 'TY' ends with 'ER' but instead met end of file"))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QStringLiteral("Expected that entry that starts with 'TY' ends with 'ER' but instead met end of file"))); } if (!value.isEmpty()) { RISitem item; item.key = key; item.value = value; result.append(item); } return result; } inline QString optionallyProtectCasing(const QString &text) const { if (protectCasing) return QLatin1Char('{') + text + QLatin1Char('}'); else return text; } Element *nextElement(QTextStream &textStream) { RISitemList list = readElement(textStream); if (list.empty()) return nullptr; QString entryType = Entry::etMisc; Entry *entry = new Entry(entryType, QString(QStringLiteral("RIS_%1")).arg(referenceCounter++)); QString journalName, startPage, endPage, date; int fieldCounter = 0; for (RISitemList::iterator it = list.begin(); it != list.end(); ++it) { if ((*it).key == QStringLiteral("TY")) { if ((*it).value.startsWith(QStringLiteral("BOOK")) || (*it).value.startsWith(QStringLiteral("SER"))) entryType = Entry::etBook; else if ((*it).value.startsWith(QStringLiteral("CHAP"))) entryType = Entry::etInBook; else if ((*it).value.startsWith(QStringLiteral("CONF"))) entryType = Entry::etInProceedings; else if ((*it).value.startsWith(QStringLiteral("JFULL")) || (*it).value.startsWith(QStringLiteral("JOUR")) || (*it).value.startsWith(QStringLiteral("MGZN"))) entryType = Entry::etArticle; else if ((*it).value.startsWith(QStringLiteral("RPRT"))) entryType = Entry::etTechReport; else if ((*it).value.startsWith(QStringLiteral("THES"))) entryType = Entry::etPhDThesis; // FIXME what about etMastersThesis? else if ((*it).value.startsWith(QStringLiteral("UNPB"))) entryType = Entry::etUnpublished; entry->setType(entryType); } else if ((*it).key == QStringLiteral("AU") || (*it).key == QStringLiteral("A1")) { Person *person = splitName((*it).value); if (person != nullptr) appendValue(entry, Entry::ftAuthor, QSharedPointer<Person>(person)); } else if ((*it).key == QStringLiteral("ED") || (*it).key == QStringLiteral("A2")) { Person *person = splitName((*it).value); if (person != nullptr) appendValue(entry, Entry::ftEditor, QSharedPointer<Person>(person)); } else if ((*it).key == QStringLiteral("ID")) { entry->setId((*it).value); } else if ((*it).key == QStringLiteral("Y1") || (*it).key == QStringLiteral("PY")) { date = (*it).value; } else if ((*it).key == QStringLiteral("Y2")) { if (date.isEmpty()) date = (*it).value; } else if ((*it).key == QStringLiteral("AB") || (*it).key == QStringLiteral("N2")) { appendValue(entry, Entry::ftAbstract, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("N1")) { appendValue(entry, Entry::ftNote, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("KW")) { QString text = (*it).value; const QRegularExpression splitRegExp(text.contains(QStringLiteral(";")) ? QStringLiteral("\\s*[;\\n]\\s*") : (text.contains(QStringLiteral(",")) ? QStringLiteral("\\s*[,\\n]\\s*") : QStringLiteral("\\n"))); QStringList newKeywords = text.split(splitRegExp, QString::SkipEmptyParts); for (QStringList::Iterator it = newKeywords.begin(); it != newKeywords.end(); ++it) appendValue(entry, Entry::ftKeywords, QSharedPointer<Keyword>(new Keyword(*it))); } else if ((*it).key == QStringLiteral("TI") || (*it).key == QStringLiteral("T1")) { appendValue(entry, Entry::ftTitle, QSharedPointer<PlainText>(new PlainText(optionallyProtectCasing((*it).value)))); } else if ((*it).key == QStringLiteral("T3")) { appendValue(entry, Entry::ftSeries, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("JO") || (*it).key == QStringLiteral("J1") || (*it).key == QStringLiteral("J2")) { if (journalName.isEmpty()) journalName = (*it).value; } else if ((*it).key == QStringLiteral("JF") || (*it).key == QStringLiteral("JA")) { journalName = (*it).value; } else if ((*it).key == QStringLiteral("VL")) { appendValue(entry, Entry::ftVolume, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("CP")) { appendValue(entry, Entry::ftChapter, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("IS")) { appendValue(entry, Entry::ftNumber, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("DO") || (*it).key == QStringLiteral("M3")) { const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match((*it).value); if (doiRegExpMatch.hasMatch()) appendValue(entry, Entry::ftDOI, QSharedPointer<VerbatimText>(new VerbatimText(doiRegExpMatch.captured()))); } else if ((*it).key == QStringLiteral("PB")) { appendValue(entry, Entry::ftPublisher, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("IN")) { appendValue(entry, Entry::ftSchool, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("SN")) { const QString fieldName = entryType == Entry::etBook || entryType == Entry::etInBook ? Entry::ftISBN : Entry::ftISSN; appendValue(entry, fieldName, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("CY")) { appendValue(entry, Entry::ftLocation, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("AD")) { appendValue(entry, Entry::ftAddress, QSharedPointer<PlainText>(new PlainText((*it).value))); } else if ((*it).key == QStringLiteral("L1") || (*it).key == QStringLiteral("L2") || (*it).key == QStringLiteral("L3") || (*it).key == QStringLiteral("UR")) { QString fieldValue = (*it).value; fieldValue.replace(QStringLiteral("<Go to ISI>://"), QStringLiteral("isi://")); const QRegularExpressionMatch doiRegExpMatch = KBibTeX::doiRegExp.match(fieldValue); const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(fieldValue); - const QString fieldName = doiRegExpMatch.hasMatch() ? Entry::ftDOI : (KBibTeX::urlRegExp.match((*it).value).hasMatch() ? Entry::ftUrl : (Preferences::instance().bibliographySystem() == Preferences::BibTeX ? Entry::ftLocalFile : Entry::ftFile)); + const QString fieldName = doiRegExpMatch.hasMatch() ? Entry::ftDOI : (KBibTeX::urlRegExp.match((*it).value).hasMatch() ? Entry::ftUrl : (Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibTeX ? Entry::ftLocalFile : Entry::ftFile)); fieldValue = doiRegExpMatch.hasMatch() ? doiRegExpMatch.captured() : (urlRegExpMatch.hasMatch() ? urlRegExpMatch.captured() : fieldValue); if (fieldValue.startsWith(QStringLiteral("file:///"))) fieldValue = fieldValue.mid(7); appendValue(entry, fieldName, QSharedPointer<VerbatimText>(new VerbatimText(fieldValue))); } else if ((*it).key == QStringLiteral("SP")) { startPage = (*it).value; } else if ((*it).key == QStringLiteral("EP")) { endPage = (*it).value; } else { const QString fieldName = QString(QStringLiteral("RISfield_%1_%2")).arg(fieldCounter++).arg((*it).key.left(2)); appendValue(entry, fieldName, QSharedPointer<PlainText>(new PlainText((*it).value))); } } if (!journalName.isEmpty()) { const QString fieldName = entryType == Entry::etInBook || entryType == Entry::etInProceedings ? Entry::ftBookTitle : Entry::ftJournal; Value value = entry->value(fieldName); value.append(QSharedPointer<PlainText>(new PlainText(optionallyProtectCasing(journalName)))); entry->insert(fieldName, value); } if (!startPage.isEmpty() || !endPage.isEmpty()) { QString page; if (startPage.isEmpty()) page = endPage; else if (endPage.isEmpty()) page = startPage; else page = startPage + QChar(0x2013) + endPage; Value value; value.append(QSharedPointer<PlainText>(new PlainText(page))); entry->insert(Entry::ftPages, value); } QStringList dateFragments = date.split(QStringLiteral("/"), QString::SkipEmptyParts); if (dateFragments.count() > 0) { bool ok; int year = dateFragments[0].toInt(&ok); if (ok && year > 1000 && year < 3000) { Value value = entry->value(Entry::ftYear); value.append(QSharedPointer<PlainText>(new PlainText(QString::number(year)))); entry->insert(Entry::ftYear, value); } else { qCWarning(LOG_KBIBTEX_IO) << "Invalid year: " << dateFragments[0]; /// Instead of an 'emit' ... - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Invalid year: '%1'")).arg(dateFragments[0]))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Invalid year: '%1'")).arg(dateFragments[0]))); } } if (dateFragments.count() > 1) { bool ok; int month = dateFragments[1].toInt(&ok); if (ok && month >= 1 && month <= 12) { Value value = entry->value(Entry::ftMonth); value.append(QSharedPointer<MacroKey>(new MacroKey(KBibTeX::MonthsTriple[month - 1]))); entry->insert(Entry::ftMonth, value); } else { qCWarning(LOG_KBIBTEX_IO) << "Invalid month: " << dateFragments[1]; /// Instead of an 'emit' ... - QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, SeverityWarning), Q_ARG(QString, QString(QStringLiteral("Invalid month: '%1'")).arg(dateFragments[1]))); + QMetaObject::invokeMethod(parent, "message", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(FileImporter::MessageSeverity, MessageSeverity::Warning), Q_ARG(QString, QString(QStringLiteral("Invalid month: '%1'")).arg(dateFragments[1]))); } } removeDuplicates(entry, Entry::ftDOI); removeDuplicates(entry, Entry::ftUrl); return entry; } void removeDuplicateValueItems(Value &value) { if (value.count() < 2) return; /// Values with one or no ValueItem cannot have duplicates QSet<QString> uniqueStrings; for (Value::Iterator it = value.begin(); it != value.end();) { const QString itemString = PlainTextValue::text(*it); if (uniqueStrings.contains(itemString)) it = value.erase(it); else { uniqueStrings.insert(itemString); ++it; } } } }; FileImporterRIS::FileImporterRIS(QObject *parent) : FileImporter(parent), d(new FileImporterRISPrivate(this)) { // nothing } FileImporterRIS::~FileImporterRIS() { delete d; } File *FileImporterRIS::load(QIODevice *iodevice) { if (!iodevice->isReadable() && !iodevice->open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Input device not readable"; - emit message(SeverityError, QStringLiteral("Input device not readable")); + emit message(MessageSeverity::Error, QStringLiteral("Input device not readable")); return nullptr; } d->cancelFlag = false; d->referenceCounter = 0; QTextStream textStream(iodevice); File *result = new File(); while (!d->cancelFlag && !textStream.atEnd()) { emit progress(textStream.pos(), iodevice->size()); QCoreApplication::instance()->processEvents(); Element *element = d->nextElement(textStream); if (element != nullptr) result->append(QSharedPointer<Element>(element)); QCoreApplication::instance()->processEvents(); } emit progress(100, 100); if (d->cancelFlag) { delete result; result = nullptr; } iodevice->close(); if (result != nullptr) result->setProperty(File::ProtectCasing, static_cast<int>(d->protectCasing ? Qt::Checked : Qt::Unchecked)); return result; } bool FileImporterRIS::guessCanDecode(const QString &text) { return text.indexOf(QStringLiteral("TY - ")) >= 0; } void FileImporterRIS::setProtectCasing(bool protectCasing) { d->protectCasing = protectCasing; } void FileImporterRIS::cancel() { d->cancelFlag = true; } diff --git a/src/io/fileinfo.cpp b/src/io/fileinfo.cpp index b3fbf9a4..5e5d839f 100644 --- a/src/io/fileinfo.cpp +++ b/src/io/fileinfo.cpp @@ -1,372 +1,372 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileinfo.h" #include <poppler-qt5.h> #include <QFileInfo> #include <QMimeDatabase> #include <QDir> #include <QTextStream> #include <QStandardPaths> #include <QRegularExpression> #include <QtConcurrentRun> #include <KBibTeX> #include <Entry> #include "logging_io.h" FileInfo::FileInfo() { /// nothing } const QString FileInfo::mimetypeOctetStream = QStringLiteral("application/octet-stream"); const QString FileInfo::mimetypeHTML = QStringLiteral("text/html"); const QString FileInfo::mimetypeBibTeX = QStringLiteral("text/x-bibtex"); const QString FileInfo::mimetypeRIS = QStringLiteral("application/x-research-info-systems"); const QString FileInfo::mimetypePDF = QStringLiteral("application/pdf"); QMimeType FileInfo::mimeTypeForUrl(const QUrl &url) { if (!url.isValid()) { qCWarning(LOG_KBIBTEX_IO) << "Cannot determine mime type for empty or invalid QUrl"; return QMimeType(); ///< invalid input gives invalid mime type } static const QMimeDatabase db; static const QMimeType mtHTML(db.mimeTypeForName(mimetypeHTML)); static const QMimeType mtOctetStream(db.mimeTypeForName(mimetypeOctetStream)); static const QMimeType mtBibTeX(db.mimeTypeForName(mimetypeBibTeX)); static const QMimeType mtPDF(db.mimeTypeForName(mimetypePDF)); static const QMimeType mtRIS(db.mimeTypeForName(mimetypeRIS)); /// Test if mime type for BibTeX is registered before determining file extension static const QString mimetypeBibTeXExt = mtBibTeX.preferredSuffix(); /// Test if mime type for RIS is registered before determining file extension static const QString mimetypeRISExt = mtRIS.preferredSuffix(); /// Test if mime type for PDF is registered before determining file extension static const QString mimetypePDFExt = mtPDF.preferredSuffix(); const QString extension = db.suffixForFileName(url.fileName()).toLower(); /// First, check preferred suffixes if (extension == mimetypeBibTeXExt) return mtBibTeX; else if (extension == mimetypeRISExt) return mtRIS; else if (extension == mimetypePDFExt) return mtPDF; /// Second, check any other suffixes else if (mtBibTeX.suffixes().contains(extension)) return mtBibTeX; else if (mtRIS.suffixes().contains(extension)) return mtRIS; else if (mtPDF.suffixes().contains(extension)) return mtPDF; /// Let the KDE subsystem guess the mime type QMimeType result = db.mimeTypeForUrl(url); /// Fall back to application/octet-stream if something goes wrong if (!result.isValid()) result = mtOctetStream; /// In case that KDE could not determine mime type, /// do some educated guesses on our own if (result.name() == mimetypeOctetStream) { if (url.scheme().startsWith(QStringLiteral("http"))) result = mtHTML; // TODO more tests? } return result; } void FileInfo::urlsInText(const QString &text, const TestExistence testExistence, const QString &baseDirectory, QSet<QUrl> &result) { if (text.isEmpty()) return; /// DOI identifiers have to extracted first as KBibTeX::fileListSeparatorRegExp /// contains characters that can be part of a DOI (e.g. ';') and thus could split /// a DOI in between. QString internalText = text; int pos = 0; QRegularExpressionMatch doiRegExpMatch; while ((doiRegExpMatch = KBibTeX::doiRegExp.match(internalText, pos)).hasMatch()) { pos = doiRegExpMatch.capturedStart(0); QString doiMatch = doiRegExpMatch.captured(0); const int semicolonHttpPos = doiMatch.indexOf(QStringLiteral(";http")); if (semicolonHttpPos > 0) doiMatch = doiMatch.left(semicolonHttpPos); const QUrl url(KBibTeX::doiUrlPrefix + QString(doiMatch).remove(QStringLiteral("\\"))); if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates /// Cut away any URL that may be right before found DOI number: /// For example, if DOI '10.1000/38-abc' was found in /// 'Lore ipsum http://doi.example.org/10.1000/38-abc Lore ipsum' /// also remove 'http://doi.example.org/' from the text, keeping only /// 'Lore ipsum Lore ipsum' static const QRegularExpression genericDoiUrlPrefix(QStringLiteral("http[s]?://[a-z0-9./-]+/$")); ///< looks like an URL const QRegularExpressionMatch genericDoiUrlPrefixMatch = genericDoiUrlPrefix.match(internalText.left(pos)); if (genericDoiUrlPrefixMatch.hasMatch()) /// genericDoiUrlPrefixMatch.captured(0) may contain (parts of) DOI internalText = internalText.left(genericDoiUrlPrefixMatch.capturedStart(0)) + internalText.mid(pos + doiMatch.length()); else internalText = internalText.left(pos) + internalText.mid(pos + doiMatch.length()); } const QStringList fileList = internalText.split(KBibTeX::fileListSeparatorRegExp, QString::SkipEmptyParts); for (const QString &text : fileList) { internalText = text; /// If testing for the actual existence of a filename found in the text ... - if (testExistence == TestExistenceYes) { + if (testExistence == TestExistence::Yes) { /// If a base directory (e.g. the location of the parent .bib file) is given /// and the potential filename fragment is NOT an absolute path, ... if (internalText.startsWith(QStringLiteral("~") + QDir::separator())) { const QString fullFilename = QDir::homePath() + internalText.mid(1); const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else if (!baseDirectory.isEmpty() && // TODO the following test assumes that absolute paths start // with a dir separator, which may only be true on Unix/Linux, // but not Windows. May be a test for 'first character is a letter, // second is ":", third is "\"' may be necessary. !internalText.startsWith(QDir::separator())) { /// To get the absolute path, prepend filename fragment with base directory const QString fullFilename = baseDirectory + QDir::separator() + internalText; const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else { /// Either the filename fragment is an absolute path OR no base directory /// was given (current working directory is assumed), ... const QFileInfo fileInfo(internalText); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// stop searching for URLs or filenames in current internal text continue; } } } /// extract URL from current field pos = 0; QRegularExpressionMatch urlRegExpMatch; while ((urlRegExpMatch = KBibTeX::urlRegExp.match(internalText, pos)).hasMatch()) { pos = urlRegExpMatch.capturedStart(0); const QString match = urlRegExpMatch.captured(0); QUrl url(match); - if (url.isValid() && (testExistence == TestExistenceNo || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) + if (url.isValid() && (testExistence == TestExistence::No || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// explicitly check URL entry, may be an URL even if http:// or alike is missing pos = 0; QRegularExpressionMatch domainNameRegExpMatch; while ((domainNameRegExpMatch = KBibTeX::domainNameRegExp.match(internalText, pos)).hasMatch()) { pos = domainNameRegExpMatch.capturedStart(0); int pos2 = internalText.indexOf(QStringLiteral(" "), pos + 1); if (pos2 < 0) pos2 = internalText.length(); QString match = internalText.mid(pos, pos2 - pos); const QUrl url(QStringLiteral("http://") + match); // FIXME what about HTTPS? if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// extract general file-like patterns pos = 0; QRegularExpressionMatch fileRegExpMatch; while ((fileRegExpMatch = KBibTeX::fileRegExp.match(internalText, pos)).hasMatch()) { pos = fileRegExpMatch.capturedStart(0); const QString match = fileRegExpMatch.captured(0); const QFileInfo fi(match); const QUrl url = QUrl::fromLocalFile(!match.startsWith(QStringLiteral("/")) && !match.startsWith(QStringLiteral("http")) && fi.isRelative() && !baseDirectory.isEmpty() ? baseDirectory + QStringLiteral("/") + match : match); - if (url.isValid() && (testExistence == TestExistenceNo || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) + if (url.isValid() && (testExistence == TestExistence::No || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } } } QSet<QUrl> FileInfo::entryUrls(const QSharedPointer<const Entry> &entry, const QUrl &bibTeXUrl, TestExistence testExistence) { QSet<QUrl> result; if (entry.isNull() || entry->isEmpty()) return result; if (entry->contains(Entry::ftDOI)) { const QString doi = PlainTextValue::text(entry->value(Entry::ftDOI)); QRegularExpressionMatch doiRegExpMatch; if (!doi.isEmpty() && (doiRegExpMatch = KBibTeX::doiRegExp.match(doi)).hasMatch()) { QString match = doiRegExpMatch.captured(0); QUrl url(KBibTeX::doiUrlPrefix + match.remove(QStringLiteral("\\"))); result.insert(url); } } static const QString etPMID = QStringLiteral("pmid"); if (entry->contains(etPMID)) { const QString pmid = PlainTextValue::text(entry->value(etPMID)); bool ok = false; ok &= pmid.toInt(&ok) > 0; if (ok) { QUrl url(QStringLiteral("https://www.ncbi.nlm.nih.gov/pubmed/") + pmid); result.insert(url); } } static const QString etEPrint = QStringLiteral("eprint"); if (entry->contains(etEPrint)) { const QString eprint = PlainTextValue::text(entry->value(etEPrint)); if (!eprint.isEmpty()) { QUrl url(QStringLiteral("http://arxiv.org/search?query=") + eprint); result.insert(url); } } const QString baseDirectory = bibTeXUrl.isValid() ? bibTeXUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() : QString(); for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { /// skip abstracts, they contain sometimes strange text fragments /// that are mistaken for URLs if (it.key().toLower() == Entry::ftAbstract) continue; const Value v = it.value(); for (const auto &valueItem : v) { QString plainText = PlainTextValue::text(*valueItem); static const QRegularExpression regExpEscapedChars = QRegularExpression(QStringLiteral("\\\\+([&_~])")); plainText.replace(regExpEscapedChars, QStringLiteral("\\1")); urlsInText(plainText, testExistence, baseDirectory, result); } } if (!baseDirectory.isEmpty()) { /// File types supported by "document preview" static const QStringList documentFileExtensions {QStringLiteral(".pdf"), QStringLiteral(".pdf.gz"), QStringLiteral(".pdf.bz2"), QStringLiteral(".ps"), QStringLiteral(".ps.gz"), QStringLiteral(".ps.bz2"), QStringLiteral(".eps"), QStringLiteral(".eps.gz"), QStringLiteral(".eps.bz2"), QStringLiteral(".html"), QStringLiteral(".xhtml"), QStringLiteral(".htm"), QStringLiteral(".dvi"), QStringLiteral(".djvu"), QStringLiteral(".wwf"), QStringLiteral(".jpeg"), QStringLiteral(".jpg"), QStringLiteral(".png"), QStringLiteral(".gif"), QStringLiteral(".tif"), QStringLiteral(".tiff")}; result.reserve(result.size() + documentFileExtensions.size() * 2); /// check if in the same directory as the BibTeX file /// a PDF file exists which filename is based on the entry's id for (const QString &extension : documentFileExtensions) { const QFileInfo fi(baseDirectory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } /// check if in the same directory as the BibTeX file there is a subdirectory /// similar to the BibTeX file's name and which contains a PDF file exists /// which filename is based on the entry's id static const QRegularExpression filenameExtension(QStringLiteral("\\.[^.]{2,5}$")); const QString basename = bibTeXUrl.fileName().remove(filenameExtension); QString directory = baseDirectory + QDir::separator() + basename; for (const QString &extension : documentFileExtensions) { const QFileInfo fi(directory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } } return result; } QString FileInfo::pdfToText(const QString &pdfFilename) { /// Build filename for text file where PDF file's plain text is cached const QString cacheDirectory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/pdftotext"); if (!QDir(cacheDirectory).exists() && !QDir::home().mkdir(cacheDirectory)) /// Could not create cache directory return QString(); static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9_]"), QRegularExpression::CaseInsensitiveOption); const QString textFilename = QString(pdfFilename).remove(invalidChars).append(QStringLiteral(".txt")).prepend(QStringLiteral("/")).prepend(cacheDirectory); /// First, check if there is a cache text file if (QFileInfo::exists(textFilename)) { /// Load text from cache file QFile f(textFilename); if (f.open(QFile::ReadOnly)) { const QString text = QString::fromUtf8(f.readAll()); f.close(); return text; } } else /// No cache file exists, so run text extraction in another thread QtConcurrent::run(extractPDFTextToCache, pdfFilename, textFilename); return QString(); } void FileInfo::extractPDFTextToCache(const QString &pdfFilename, const QString &cacheFilename) { /// In case of multiple calls, skip text extraction if cache file already exists if (QFile(cacheFilename).exists()) return; QString text; QStringList msgList; /// Load PDF file through Poppler Poppler::Document *doc = Poppler::Document::load(pdfFilename); if (doc != nullptr) { static const int maxPages = 64; /// Build text by appending each page's text for (int i = 0; i < qMin(maxPages, doc->numPages()); ++i) text.append(doc->page(i)->text(QRect())).append(QStringLiteral("\n\n")); if (doc->numPages() > maxPages) msgList << QString(QStringLiteral("### Skipped %1 pages as PDF file contained too many pages (limit is %2 pages) ###")).arg(doc->numPages() - maxPages).arg(maxPages); delete doc; } else msgList << QStringLiteral("### Skipped as file could not be opened as PDF file ###"); /// Save text in cache file QFile f(cacheFilename); if (f.open(QFile::WriteOnly)) { static const int maxCharacters = 1 << 18; f.write(text.left(maxCharacters).toUtf8()); ///< keep only the first 2^18 many characters if (text.length() > maxCharacters) msgList << QString(QStringLiteral("### Text too long, skipping %1 characters ###")).arg(text.length() - maxCharacters); /// Write all messages (warnings) to end of text file for (const QString &msg : const_cast<const QStringList &>(msgList)) { static const char linebreak = '\n'; f.write(&linebreak, 1); f.write(msg.toUtf8()); } f.close(); } } diff --git a/src/io/fileinfo.h b/src/io/fileinfo.h index 4384f337..ecc168d6 100644 --- a/src/io/fileinfo.h +++ b/src/io/fileinfo.h @@ -1,105 +1,105 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEINFO_H #define KBIBTEX_IO_FILEINFO_H #include <QSet> #include <QUrl> #include <QMimeType> #include <QSharedPointer> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 class Entry; class KBIBTEXIO_EXPORT FileInfo { public: static const QString mimetypeOctetStream; static const QString mimetypeHTML; static const QString mimetypeBibTeX; static const QString mimetypeRIS; static const QString mimetypePDF; - enum TestExistence { - TestExistenceYes, ///< Test if file exists - TestExistenceNo ///< Skip test if file exists + enum class TestExistence { + Yes, ///< Test if file exists + No ///< Skip test if file exists }; /** * Finds a QMimeType with the given url. * Tries to guess a file's mime type by its extension first, * but falls back to QMimeType's mimeTypeForName if that does * not work. Background: If a HTTP or WebDAV server claims * that a .bib file is of mime type application/octet-stream, * QMimeType::mimeTypeForName will keep that assessment * instead of inspecting the file extension. * * @see QMimeType::mimeTypeForName * @param url Url to analyze * @return Guessed mime type */ static QMimeType mimeTypeForUrl(const QUrl &url); /** * Find all file or URL references in the given text. Found filenames or * URLs are appended to the addTo list (duplicates are avoided). * Different test may get performed depending of the test for existence * of a potential file should be checked or not checked or if this matter * is undecided/irrelevant (recommended default case). For the test of * existence, baseDirectory is used to resolve relative paths. * @param text text to scan for filenames or URLs * @param testExistence shall be tested for file existence? * @param baseDirectory base directory for tests on relative path names * @param addTo add found URLs/filenames to this list */ static void urlsInText(const QString &text, const TestExistence testExistence, const QString &baseDirectory, QSet<QUrl> &addTo); /** * Find all file or URL references in the given entry. Found filenames or * URLs are appended to the addTo list (duplicates are avoided). * Different test may get performed depending of the test for existence * of a potential file should be checked or not checked or if this matter * is undecided/irrelevant (recommended default case). For the test of * existence, bibTeXUrl is used to resolve relative paths. * @param entry entry to scan for filenames or URLs * @param bibTeXUrl base directory/URL for tests on relative path names * @param testExistence shall be tested for file existence? * @return list of found URLs/filenames (duplicates are avoided) */ static QSet<QUrl> entryUrls(const QSharedPointer<const Entry> &entry, const QUrl &bibTeXUrl, TestExistence testExistence); /** * Load the given PDF file and return the contained plain text. * Makes use of Poppler to load and parse the file. All text * will be cached and loaded from cache if possible. * @param pdfFilename PDF file to load and extract text from * @return extracted plain text, either directly from PDF file or from cache OR QString() if there was an error */ static QString pdfToText(const QString &pdfFilename); protected: FileInfo(); private: static void extractPDFTextToCache(const QString &pdfFilename, const QString &cacheFilename); }; #endif // KBIBTEX_IO_FILEINFO_H diff --git a/src/io/textencoder.cpp b/src/io/textencoder.cpp index 5bd72656..7d1a52c2 100644 --- a/src/io/textencoder.cpp +++ b/src/io/textencoder.cpp @@ -1,68 +1,68 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "textencoder.h" #include <QStringList> #include <QTextCodec> #include "encoderlatex.h" #include "logging_io.h" TextEncoder::TextEncoder() { /// nothing } QByteArray TextEncoder::encode(const QString &input, const QString &destinationEncoding) { QTextCodec *destinationCodec(QTextCodec::codecForName(destinationEncoding.toLatin1())); return encode(input, destinationCodec); } QByteArray TextEncoder::encode(const QString &input, const QTextCodec *destinationCodec) { /// Invalid codec? Cannot do anything if (destinationCodec == nullptr) return QByteArray(); /// Perform Canonical Decomposition followed by Canonical Composition const QString ninput = input.normalized(QString::NormalizationForm_C); QByteArray result; const Encoder &laTeXEncoder = EncoderLaTeX::instance(); /// Build result, character by character for (const QChar &c : ninput) { /// Get byte sequence representing current character in chosen codec const QByteArray cba = destinationCodec->fromUnicode(c); if (destinationCodec->canEncode(c) && (c == QChar(0x003f /** question mark */) || cba.size() != 1 || cba[0] != 0x3f /** question mark */)) { /// Codec claims that it can encode current character, but some codecs /// still cannot encode character and simply return a question mark, so /// only accept question marks as encoding result if original character /// was question mark (assuming all codecs can encode question marks). result.append(cba); } else { /// Chosen codec can NOT encode current Unicode character, so try to use /// 'LaTeX encoder', which may translate 0x00c5 (A with ring above) into /// '\AA'. LaTeX encoder returns UTF-8 representation if given character /// cannot be encoded - result.append(laTeXEncoder.encode(QString(c), Encoder::TargetEncodingASCII).toUtf8()); + result.append(laTeXEncoder.encode(QString(c), Encoder::TargetEncoding::ASCII).toUtf8()); } } return result; } diff --git a/src/networking/associatedfiles.cpp b/src/networking/associatedfiles.cpp index f867ff67..3e9a6b4e 100644 --- a/src/networking/associatedfiles.cpp +++ b/src/networking/associatedfiles.cpp @@ -1,170 +1,170 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "associatedfiles.h" #include <QFileInfo> #include <QDir> #include <KIO/CopyJob> #include <KJobWidgets> #include <Preferences> #include "logging_networking.h" QString AssociatedFiles::relativeFilename(const QUrl &documentUrl, const QUrl &baseUrl) { if (!documentUrl.isValid()) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL but is invalid"; return QString(); } if (!baseUrl.isValid() || baseUrl.isRelative()) { qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute file location or URL and must be valid"; return documentUrl.url(QUrl::PreferLocalFile); } if (documentUrl.scheme() != baseUrl.scheme() || (documentUrl.scheme() != QStringLiteral("file") && documentUrl.host() != baseUrl.host())) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL and base URL do not match (protocol, host, ...)"; return documentUrl.url(QUrl::PreferLocalFile); } /// First, resolve the provided document URL to an absolute URL /// using the given base URL QUrl internaldocumentUrl = documentUrl; if (internaldocumentUrl.isRelative()) internaldocumentUrl = baseUrl.resolved(internaldocumentUrl); /// Get the absolute path of the base URL const QString baseUrlDirectory = QFileInfo(baseUrl.path()).absolutePath(); /// Let QDir calculate the relative directory return QDir(baseUrlDirectory).relativeFilePath(internaldocumentUrl.path()); } QString AssociatedFiles::absoluteFilename(const QUrl &documentUrl, const QUrl &baseUrl) { if (!documentUrl.isValid()) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL but is invalid"; return QString(); } if (documentUrl.isRelative() && (!baseUrl.isValid() || baseUrl.isRelative())) { qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute, valid file location or URL if the document URL is relative"; return documentUrl.url(QUrl::PreferLocalFile); } if (documentUrl.isRelative() && (documentUrl.scheme() != baseUrl.scheme() || (documentUrl.scheme() != QStringLiteral("file") && documentUrl.host() != baseUrl.host()))) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL and base URL do not match (protocol, host, ...), but necessary if the document URL is relative"; return documentUrl.url(QUrl::PreferLocalFile); } /// Resolve the provided document URL to an absolute URL /// using the given base URL QUrl internaldocumentUrl = documentUrl; if (internaldocumentUrl.isRelative()) internaldocumentUrl = baseUrl.resolved(internaldocumentUrl); return internaldocumentUrl.url(QUrl::PreferLocalFile); } QString AssociatedFiles::insertUrl(const QUrl &documentUrl, QSharedPointer<Entry> &entry, const File *bibTeXFile, PathType pathType) { const QString finalUrl = computeAssociateUrl(documentUrl, bibTeXFile, pathType); bool alreadyContained = false; for (QMap<QString, Value>::ConstIterator it = entry->constBegin(); !alreadyContained && it != entry->constEnd(); ++it) { const Value v = it.value(); for (Value::ConstIterator vit = v.constBegin(); !alreadyContained && vit != v.constEnd(); ++vit) { if (PlainTextValue::text(*vit) == finalUrl) alreadyContained = true; } } if (!alreadyContained) { - const QString field = documentUrl.isLocalFile() ? (Preferences::instance().bibliographySystem() == Preferences::instance().BibTeX ? Entry::ftLocalFile : Entry::ftFile) : Entry::ftUrl; + const QString field = documentUrl.isLocalFile() ? (Preferences::instance().bibliographySystem() == Preferences::BibliographySystem::BibTeX ? Entry::ftLocalFile : Entry::ftFile) : Entry::ftUrl; Value value = entry->value(field); value.append(QSharedPointer<VerbatimText>(new VerbatimText(finalUrl))); entry->insert(field, value); } return finalUrl; } QString AssociatedFiles::computeAssociateUrl(const QUrl &documentUrl, const File *bibTeXFile, PathType pathType) { Q_ASSERT(bibTeXFile != nullptr); // FIXME more graceful? const QUrl baseUrl = bibTeXFile->property(File::Url).toUrl(); - if (!baseUrl.isValid() && pathType == ptRelative) { + if (!baseUrl.isValid() && pathType == PathType::Relative) { /// If no base URL was given but still a relative path was requested, /// revert choice and enforce the generation of an absolute one - pathType = ptAbsolute; + pathType = PathType::Absolute; } - const QString finalUrl = pathType == ptAbsolute ? absoluteFilename(documentUrl, baseUrl) : relativeFilename(documentUrl, baseUrl); + const QString finalUrl = pathType == PathType::Absolute ? absoluteFilename(documentUrl, baseUrl) : relativeFilename(documentUrl, baseUrl); return finalUrl; } QPair<QUrl, QUrl> AssociatedFiles::computeSourceDestinationUrls(const QUrl &sourceUrl, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, const QString &userDefinedFilename) { Q_ASSERT(bibTeXFile != nullptr); // FIXME more graceful? - if (entryId.isEmpty() && renameOperation == roEntryId) { + if (entryId.isEmpty() && renameOperation == RenameOperation::EntryId) { /// If no entry id was given but still a rename after entry id was requested, /// revert choice and enforce keeping the original name - renameOperation = roKeepName; + renameOperation = RenameOperation::KeepName; } const QUrl baseUrl = bibTeXFile->property(File::Url).toUrl(); const QUrl internalSourceUrl = baseUrl.resolved(sourceUrl); const QFileInfo internalSourceInfo(internalSourceUrl.path()); QString filename = internalSourceInfo.fileName(); QString suffix = internalSourceInfo.suffix(); if (suffix.isEmpty()) { suffix = QStringLiteral("html"); filename.append(QLatin1Char('.')).append(suffix); } if (filename.isEmpty()) filename = internalSourceUrl.url(QUrl::PreferLocalFile).remove(QDir::separator()).remove(QLatin1Char('/')).remove(QLatin1Char(':')).remove(QLatin1Char('.')) + QStringLiteral(".") + suffix; if (!bibTeXFile->hasProperty(File::Url)) return QPair<QUrl, QUrl>(); /// no valid URL set of BibTeX file object QUrl targetUrl = bibTeXFile->property(File::Url).toUrl(); if (!targetUrl.isValid()) return QPair<QUrl, QUrl>(); /// no valid URL set of BibTeX file object const QString targetPath = QFileInfo(targetUrl.path()).absolutePath(); - targetUrl.setPath(targetPath + QDir::separator() + (renameOperation == roEntryId ? entryId + QStringLiteral(".") + suffix : (renameOperation == roUserDefined ? userDefinedFilename : filename))); + targetUrl.setPath(targetPath + QDir::separator() + (renameOperation == RenameOperation::EntryId ? entryId + QStringLiteral(".") + suffix : (renameOperation == RenameOperation::UserDefined ? userDefinedFilename : filename))); return QPair<QUrl, QUrl>(internalSourceUrl, targetUrl); } QUrl AssociatedFiles::copyDocument(const QUrl &sourceUrl, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, MoveCopyOperation moveCopyOperation, QWidget *widget, const QString &userDefinedFilename) { const QPair<QUrl, QUrl> r = computeSourceDestinationUrls(sourceUrl, entryId, bibTeXFile, renameOperation, userDefinedFilename); const QUrl internalSourceUrl = r.first, targetUrl = r.second; bool success = true; if (internalSourceUrl.isLocalFile() && targetUrl.isLocalFile()) { QFile(targetUrl.path()).remove(); success &= QFile::copy(internalSourceUrl.path(), targetUrl.path()); - if (success && moveCopyOperation == mcoMove) { + if (success && moveCopyOperation == MoveCopyOperation::Move) { success &= QFile(internalSourceUrl.path()).remove(); } } else if (internalSourceUrl.isValid() && targetUrl.isValid()) { // FIXME non-blocking - KIO::CopyJob *moveCopyJob = moveCopyOperation == mcoMove ? KIO::move(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite) : KIO::copy(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite); + KIO::CopyJob *moveCopyJob = moveCopyOperation == MoveCopyOperation::Move ? KIO::move(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite) : KIO::copy(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(moveCopyJob, widget); success &= moveCopyJob->exec(); } else { qCWarning(LOG_KBIBTEX_NETWORKING) << "Either sourceUrl or targetUrl is invalid"; return QUrl(); } if (!success) return QUrl(); ///< either copy/move or delete operation failed return targetUrl; } diff --git a/src/networking/associatedfiles.h b/src/networking/associatedfiles.h index a6f89ccd..91afbb65 100644 --- a/src/networking/associatedfiles.h +++ b/src/networking/associatedfiles.h @@ -1,130 +1,130 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ASSOCIATEDFILES_H #define KBIBTEX_NETWORKING_ASSOCIATEDFILES_H #include <QUrl> #include <Entry> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 class QWidget; class File; /** * Given a remote or local filename/URL, this class will, (1) at the user's * discretion, move or copy this file next to the bibliography's file into * the same directory, (2) rename the copied file (again, at the user's * discretion) to either match the corresponding entry's id or follow a name * provided by the user and (3) modify the entry to include a reference * (either relative or absolute path) to the newly moved/copied file or its * original filename/URL. * * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT AssociatedFiles { public: - enum PathType { - ptAbsolute = 0, ///< Use absolute filenames/paths if possible - ptRelative = 1 ///< Use relative filenames/paths if possible + enum class PathType { + Absolute, ///< Use absolute filenames/paths if possible + Relative ///< Use relative filenames/paths if possible }; - enum RenameOperation { - roKeepName = 0, ///< Do not rename a file - roEntryId = 1, ///< Rename the file following the entry's id - roUserDefined = 2 ///< Rename after a string provided by the user + enum class RenameOperation { + KeepName, ///< Do not rename a file + EntryId, ///< Rename the file following the entry's id + UserDefined ///< Rename after a string provided by the user }; - enum MoveCopyOperation { - mcoNoCopyMove = 0, ///< Do not move or copy a file, use a reference only - mcoCopy = 1, ///< Copy the file next to the bibiliograpy file - mcoMove = 2 /// Same as copy, but delete original + enum class MoveCopyOperation { + None, ///< Do not move or copy a file, use a reference only + Copy, ///< Copy the file next to the bibiliograpy file + Move /// Same as copy, but delete original }; /** * Based on a given URL to an external document, compute an URL used for association * and insert it into the given entry, either as local file or as URL. * * @param documentUrl URL to a document like 'http://www.example.com/publication.pdf' * @param entry bibliography entry where the URL is to be associated with * @param bibTeXFile valid bibliography, preferrably with property 'File::Url' set * @param pathType request either a relative or an absolute path * @return the computed URL string */ static QString insertUrl(const QUrl &documentUrl, QSharedPointer<Entry> &entry, const File *bibTeXFile, PathType pathType); /** * Compute how the URL string to be associated to a bibliographic entry may look * like for a given document URL, a given bibliography, and whether the URL string * should be preferrably relative or absolute. * @param documentUrl URL to a document like 'http://www.example.com/publication.pdf' * @param bibTeXFile valid bibliography, preferrably with property 'File::Url' set * @param pathType request either a relative or an absolute path * @return the computed URL string */ static QString computeAssociateUrl(const QUrl &documentUrl, const File *bibTeXFile, PathType pathType); /** * For a given (remote) source URL and given various information such as which * bibliographic entry and file the local copy will be associated with, determine * a destination URL where the source document may be copied to. * This function will neither modify the bibliographic entry or file, nor do the * actual copying. * * @param sourceUrl The remote location of the document * @param entryId the identifier of the bibliography entry * @param bibTeXFile the bibliographic file * @param renameOperation what type of renaming is requested * @param userDefinedFilename an optional custom basename * @return A pair of URLs: refined source URL and computed destination URL */ static QPair<QUrl, QUrl> computeSourceDestinationUrls(const QUrl &sourceUrl, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, const QString &userDefinedFilename); static QUrl copyDocument(const QUrl &document, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, MoveCopyOperation moveCopyOperation, QWidget *widget, const QString &userDefinedFilename = QString()); private: /** * Translate a given URL of a document (e.g. a PDF file) to a string * representation pointing to the relative location of this document. * A "base URL", i.e. the bibliography's file location has to be provided * in order to calculate the relative location of the document. * "Upwards relativity" (i.e. paths containing "..") is not supported for this * functions output; in this case, an absolute path will be generated as fallback. * * @param document The document's URL * @param baseUrl The base URL * @return The document URL's string representation relative to the base URL */ static QString relativeFilename(const QUrl &document, const QUrl &baseUrl); /** * Translate a given URL of a document (e.g. a PDF file) to a string * representation pointing to the absolute location of this document. * A "base URL", i.e. the bibliography's file location may be provided to * resolve relative document URLs. * * @param document The document's URL * @param baseUrl The base URL * @return The document URL's string representation in absolute form */ static QString absoluteFilename(const QUrl &document, const QUrl &baseUrl); }; #endif // KBIBTEX_NETWORKING_ASSOCIATEDFILES_H diff --git a/src/networking/findpdf.cpp b/src/networking/findpdf.cpp index c1f7f52a..15f70e3c 100644 --- a/src/networking/findpdf.cpp +++ b/src/networking/findpdf.cpp @@ -1,455 +1,455 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "findpdf.h" #include <QNetworkReply> #include <QNetworkRequest> #include <QRegularExpression> #include <QApplication> #include <QTemporaryFile> #include <QUrlQuery> #include <QStandardPaths> #include <QDir> #include <poppler-qt5.h> #include <KBibTeX> #include <Value> #include <FileInfo> #include "internalnetworkaccessmanager.h" #include "logging_networking.h" int maxDepth = 5; static const char *depthProperty = "depth"; static const char *termProperty = "term"; static const char *originProperty = "origin"; class FindPDF::Private { private: FindPDF *p; public: int aliveCounter; QList<ResultItem> result; Entry currentEntry; QSet<QUrl> knownUrls; QSet<QNetworkReply *> runningDownloads; Private(FindPDF *parent) : p(parent), aliveCounter(0) { /// nothing } bool queueUrl(const QUrl &url, const QString &term, const QString &origin, int depth) { if (!knownUrls.contains(url) && depth > 0) { knownUrls.insert(url); QNetworkRequest request = QNetworkRequest(url); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply, 15); ///< set a timeout on network connections reply->setProperty(depthProperty, QVariant::fromValue<int>(depth)); reply->setProperty(termProperty, term); reply->setProperty(originProperty, origin); runningDownloads.insert(reply); connect(reply, &QNetworkReply::finished, p, &FindPDF::downloadFinished); ++aliveCounter; return true; } else return false; } void processGeneralHTML(QNetworkReply *reply, const QString &text) { /// fetch some properties from Reply object const QString term = reply->property(termProperty).toString(); const QString origin = reply->property(originProperty).toString(); bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; /// regular expressions to guess links to follow const QVector<QRegularExpression> specificAnchorRegExp = { QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]*%1[^\"]*[.]pdf)\"")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]+)\"[^>]*>[^<]*%1[^<]*[.]pdf")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]*%1[^\"]*)\"")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]+)\"[^>]*>[^<]*%1[^<]*\\b")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption) }; static const QRegularExpression genericAnchorRegExp = QRegularExpression(QStringLiteral("<a[^>]*href=\"([^\"]+)\""), QRegularExpression::CaseInsensitiveOption); bool gotLink = false; for (const QRegularExpression &anchorRegExp : specificAnchorRegExp) { const QRegularExpressionMatch match = anchorRegExp.match(text); if (match.hasMatch()) { const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), term, origin, depth - 1); gotLink = true; break; } } if (!gotLink) { /// this is only the last resort: /// to follow the first link found in the HTML document const QRegularExpressionMatch match = genericAnchorRegExp.match(text); if (match.hasMatch()) { const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), term, origin, depth - 1); } } } void processGoogleResult(QNetworkReply *reply, const QString &text) { static const QString h3Tag(QStringLiteral("<h3")); static const QString aTag(QStringLiteral("<a")); static const QString hrefAttrib(QStringLiteral("href=\"")); const QString term = reply->property(termProperty).toString(); bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; /// extract the first numHitsToFollow-many hits found by Google Scholar const int numHitsToFollow = 10; int p = -1; for (int i = 0; i < numHitsToFollow; ++i) { if ((p = text.indexOf(h3Tag, p + 1)) >= 0 && (p = text.indexOf(aTag, p + 1)) >= 0 && (p = text.indexOf(hrefAttrib, p + 1)) >= 0) { int p1 = p + 6; int p2 = text.indexOf(QLatin1Char('"'), p1 + 1); QUrl url(text.mid(p1, p2 - p1)); const QString googleService = reply->url().host().contains(QStringLiteral("scholar.google")) ? QStringLiteral("scholar.google") : QStringLiteral("www.google"); queueUrl(reply->url().resolved(url), term, googleService, depth - 1); } } } void processSpringerLink(QNetworkReply *reply, const QString &text) { static const QRegularExpression fulltextPDFlink(QStringLiteral("href=\"([^\"]+/fulltext.pdf)\"")); const QRegularExpressionMatch match = fulltextPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url(match.captured(1)); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("springerlink"), depth - 1); } } void processCiteSeerX(QNetworkReply *reply, const QString &text) { static const QRegularExpression downloadPDFlink(QStringLiteral("href=\"(/viewdoc/download[^\"]+type=pdf)\"")); const QRegularExpressionMatch match = downloadPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("citeseerx"), depth - 1); } } void processACMDigitalLibrary(QNetworkReply *reply, const QString &text) { static const QRegularExpression downloadPDFlink(QStringLiteral("href=\"(ft_gateway.cfm\\?id=\\d+&ftid=\\d+&dwn=1&CFID=\\d+&CFTOKEN=\\d+)\"")); const QRegularExpressionMatch match = downloadPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("acmdl"), depth - 1); } } bool processPDF(QNetworkReply *reply, const QByteArray &data) { bool progress = false; const QString origin = reply->property(originProperty).toString(); const QUrl url = reply->url(); /// Search for duplicate URLs bool containsUrl = false; for (const ResultItem &ri : const_cast<const QList<ResultItem> &>(result)) { containsUrl |= ri.url == url; /// Skip already visited URLs if (containsUrl) break; } if (!containsUrl) { Poppler::Document *doc = Poppler::Document::loadFromData(data); ResultItem resultItem; resultItem.tempFilename = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_findpdf_XXXXXX.pdf")); resultItem.tempFilename->setAutoRemove(true); if (resultItem.tempFilename->open()) { const int lenDataWritten = resultItem.tempFilename->write(data); resultItem.tempFilename->close(); if (lenDataWritten != data.length()) { /// Failed to write to temporary file qCWarning(LOG_KBIBTEX_NETWORKING) << "Failed to write to temporary file for filename" << resultItem.tempFilename->fileName(); delete resultItem.tempFilename; resultItem.tempFilename = nullptr; } } else { /// Failed to create temporary file qCWarning(LOG_KBIBTEX_NETWORKING) << "Failed to create temporary file for templaet" << resultItem.tempFilename->fileTemplate(); delete resultItem.tempFilename; resultItem.tempFilename = nullptr; } resultItem.url = url; resultItem.textPreview = doc->info(QStringLiteral("Title")).simplified(); static const int maxTextLen = 1024; for (int i = 0; i < doc->numPages() && resultItem.textPreview.length() < maxTextLen; ++i) { Poppler::Page *page = doc->page(i); if (!resultItem.textPreview.isEmpty()) resultItem.textPreview += QLatin1Char(' '); resultItem.textPreview += page->text(QRect()).simplified().leftRef(maxTextLen); delete page; } resultItem.textPreview.remove(QStringLiteral("Microsoft Word - ")); ///< Some word processors need to put their name everywhere ... - resultItem.downloadMode = NoDownload; + resultItem.downloadMode = DownloadMode::No; resultItem.relevance = origin == Entry::ftDOI ? 1.0 : (origin == QStringLiteral("eprint") ? 0.75 : 0.5); result << resultItem; progress = true; delete doc; } return progress; } QUrl ieeeDocumentUrlToDownloadUrl(const QUrl &url) { /// Basic checking if provided URL is from IEEE Xplore if (!url.host().contains(QStringLiteral("ieeexplore.ieee.org"))) return url; /// Assuming URL looks like this: /// http://ieeexplore.ieee.org/document/8092651/ static const QRegularExpression documentIdRegExp(QStringLiteral("/(\\d{6,})/$")); const QRegularExpressionMatch documentIdRegExpMatch = documentIdRegExp.match(url.path()); if (!documentIdRegExpMatch.hasMatch()) return url; /// Use document id extracted above to build URL to PDF file return QUrl(QStringLiteral("http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=") + documentIdRegExpMatch.captured(1)); } }; FindPDF::FindPDF(QObject *parent) : QObject(parent), d(new Private(this)) { /// nothing } FindPDF::~FindPDF() { abort(); delete d; } bool FindPDF::search(const Entry &entry) { if (d->aliveCounter > 0) return false; d->knownUrls.clear(); d->result.clear(); d->currentEntry = entry; emit progress(0, d->aliveCounter, 0); /// Generate a string which contains the title's beginning QString searchWords; if (entry.contains(Entry::ftTitle)) { const QStringList titleChunks = PlainTextValue::text(entry.value(Entry::ftTitle)).split(QStringLiteral(" "), QString::SkipEmptyParts); if (!titleChunks.isEmpty()) { searchWords = titleChunks[0]; for (int i = 1; i < titleChunks.count() && searchWords.length() < 64; ++i) searchWords += QLatin1Char(' ') + titleChunks[i]; } } const QStringList authors = entry.authorsLastName(); for (int i = 0; i < authors.count() && searchWords.length() < 96; ++i) searchWords += QLatin1Char(' ') + authors[i]; searchWords.remove(QLatin1Char('{')).remove(QLatin1Char('}')); QStringList urlFields {Entry::ftDOI, Entry::ftUrl, QStringLiteral("ee")}; for (int i = 2; i < 256; ++i) urlFields << QString(QStringLiteral("%1%2")).arg(Entry::ftDOI).arg(i) << QString(QStringLiteral("%1%2")).arg(Entry::ftUrl).arg(i); for (const QString &field : const_cast<const QStringList &>(urlFields)) { if (entry.contains(field)) { const QString fieldText = PlainTextValue::text(entry.value(field)); QRegularExpressionMatchIterator doiRegExpMatchIt = KBibTeX::doiRegExp.globalMatch(fieldText); while (doiRegExpMatchIt.hasNext()) { const QRegularExpressionMatch doiRegExpMatch = doiRegExpMatchIt.next(); d->queueUrl(QUrl(KBibTeX::doiUrlPrefix + doiRegExpMatch.captured(0)), fieldText, Entry::ftDOI, maxDepth); } QRegularExpressionMatchIterator urlRegExpMatchIt = KBibTeX::urlRegExp.globalMatch(fieldText); while (urlRegExpMatchIt.hasNext()) { QRegularExpressionMatch urlRegExpMatch = urlRegExpMatchIt.next(); d->queueUrl(QUrl(urlRegExpMatch.captured(0)), searchWords, Entry::ftUrl, maxDepth); } } } if (entry.contains(QStringLiteral("eprint"))) { /// check eprint fields as used for arXiv const QString eprintId = PlainTextValue::text(entry.value(QStringLiteral("eprint"))); if (!eprintId.isEmpty()) { const QUrl arxivUrl = QUrl::fromUserInput(QStringLiteral("http://arxiv.org/find/all/1/all:+") + eprintId + QStringLiteral("/0/1/0/all/0/1")); d->queueUrl(arxivUrl, eprintId, QStringLiteral("eprint"), maxDepth); } } if (!searchWords.isEmpty()) { /// Search in Google const QUrl googleUrl = QUrl::fromUserInput(QStringLiteral("https://www.google.com/search?hl=en&sa=G&q=filetype:pdf ") + searchWords); d->queueUrl(googleUrl, searchWords, QStringLiteral("www.google"), maxDepth); /// Search in Google Scholar const QUrl googleScholarUrl = QUrl::fromUserInput(QStringLiteral("https://scholar.google.com/scholar?hl=en&btnG=Search&as_sdt=1&q=filetype:pdf ") + searchWords); d->queueUrl(googleScholarUrl, searchWords, QStringLiteral("scholar.google"), maxDepth); /// Search in Bing const QUrl bingUrl = QUrl::fromUserInput(QStringLiteral("https://www.bing.com/search?setlang=en-US&q=filetype:pdf ") + searchWords); d->queueUrl(bingUrl, searchWords, QStringLiteral("bing"), maxDepth); /// Search in CiteSeerX const QUrl citeseerXurl = QUrl::fromUserInput(QStringLiteral("http://citeseerx.ist.psu.edu/search?submit=Search&sort=rlv&t=doc&q=") + searchWords); d->queueUrl(citeseerXurl, searchWords, QStringLiteral("citeseerx"), maxDepth); /// Search in StartPage const QUrl startPageUrl = QUrl::fromUserInput(QStringLiteral("https://www.startpage.com/do/asearch?cat=web&cmd=process_search&language=english&engine0=v1all&abp=-1&t=white&nj=1&prf=23ad6aab054a88d3da5c443280cee596&suggestOn=0&query=filetype:pdf ") + searchWords); d->queueUrl(startPageUrl, searchWords, QStringLiteral("startpage"), maxDepth); } if (d->aliveCounter == 0) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Directly at start, no URLs are queue for a search -> this should never happen"; emit finished(); } return true; } QList<FindPDF::ResultItem> FindPDF::results() { if (d->aliveCounter == 0) return d->result; else { /// Return empty list while search is running return QList<FindPDF::ResultItem>(); } } void FindPDF::abort() { QSet<QNetworkReply *>::Iterator it = d->runningDownloads.begin(); while (it != d->runningDownloads.end()) { QNetworkReply *reply = *it; it = d->runningDownloads.erase(it); reply->abort(); } } void FindPDF::downloadFinished() { static const char *htmlHead1 = "<html", *htmlHead2 = "<HTML", *htmlHead3 = "<!doctype html>" /** ACM Digital Library */; static const char *pdfHead = "%PDF-"; --d->aliveCounter; emit progress(d->knownUrls.count(), d->aliveCounter, d->result.count()); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); d->runningDownloads.remove(reply); const QString term = reply->property(termProperty).toString(); const QString origin = reply->property(originProperty).toString(); bool depthOk = false; int depth = reply->property(depthProperty).toInt(&depthOk); if (!depthOk) depth = 0; if (reply->error() == QNetworkReply::NoError) { const QByteArray data = reply->readAll(); QUrl redirUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); redirUrl = redirUrl.isValid() ? reply->url().resolved(redirUrl) : QUrl(); qCDebug(LOG_KBIBTEX_NETWORKING) << "finished Downloading " << reply->url().toDisplayString() << " depth=" << depth << " d->aliveCounter=" << d->aliveCounter << " data.size=" << data.size() << " redirUrl=" << redirUrl.toDisplayString() << " origin=" << origin; if (redirUrl.isValid()) { redirUrl = d->ieeeDocumentUrlToDownloadUrl(redirUrl); d->queueUrl(redirUrl, term, origin, depth - 1); } else if (data.contains(htmlHead1) || data.contains(htmlHead2) || data.contains(htmlHead3)) { /// returned data is a HTML file, i.e. contains "<html" /// check for limited depth before continuing if (depthOk && depth > 0) { /// Get webpage as plain text /// Assume UTF-8 data const QString text = QString::fromUtf8(data.constData()); /// regular expression to check if this is a Google Scholar result page static const QRegularExpression googleScholarTitleRegExp(QStringLiteral("<title>[^>]* - Google Scholar</title>")); /// regular expression to check if this is a SpringerLink page static const QRegularExpression springerLinkTitleRegExp(QStringLiteral("<title>[^>]* - Springer - [^>]*</title>")); /// regular expression to check if this is a CiteSeerX page static const QRegularExpression citeseerxTitleRegExp(QStringLiteral("<title>CiteSeerX &mdash; [^>]*</title>")); /// regular expression to check if this is a ACM Digital Library page static const QString acmDigitalLibraryString(QStringLiteral("The ACM Digital Library is published by the Association for Computing Machinery")); if (googleScholarTitleRegExp.match(text).hasMatch()) d->processGoogleResult(reply, text); else if (springerLinkTitleRegExp.match(text).hasMatch()) d->processSpringerLink(reply, text); else if (citeseerxTitleRegExp.match(text).hasMatch()) d->processCiteSeerX(reply, text); else if (text.contains(acmDigitalLibraryString)) d->processACMDigitalLibrary(reply, text); else { /// regular expression to extract title static const QRegularExpression titleRegExp(QStringLiteral("<title>(.*?)</title>")); const QRegularExpressionMatch match = titleRegExp.match(text); if (match.hasMatch()) qCDebug(LOG_KBIBTEX_NETWORKING) << "Using general HTML processor for page" << match.captured(1) << " URL=" << reply->url().toDisplayString(); else qCDebug(LOG_KBIBTEX_NETWORKING) << "Using general HTML processor for URL=" << reply->url().toDisplayString(); d->processGeneralHTML(reply, text); } } } else if (data.contains(pdfHead)) { /// looks like a PDF file -> grab it const bool gotPDFfile = d->processPDF(reply, data); if (gotPDFfile) emit progress(d->knownUrls.count(), d->aliveCounter, d->result.count()); } else { /// Assume UTF-8 data const QString text = QString::fromUtf8(data.constData()); qCWarning(LOG_KBIBTEX_NETWORKING) << "don't know how to handle " << text.left(256); } } else qCWarning(LOG_KBIBTEX_NETWORKING) << "error from reply: " << reply->errorString() << "(" << reply->url().toDisplayString() << ")" << " term=" << term << " origin=" << origin << " depth=" << depth; if (d->aliveCounter == 0) { /// no more running downloads left emit finished(); } } diff --git a/src/networking/findpdf.h b/src/networking/findpdf.h index 815b7f9f..79338bbe 100644 --- a/src/networking/findpdf.h +++ b/src/networking/findpdf.h @@ -1,114 +1,114 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_FINDPDF_H #define KBIBTEX_NETWORKING_FINDPDF_H #include "kbibtexnetworking_export.h" #include <QObject> #include <QList> #include <QSet> #include <QUrl> #include <Entry> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 class QNetworkAccessManager; class QNetworkReply; class QTemporaryFile; /** * Search known Internet resources (search engines) for PDF files * matching a given bibliography entry. * * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT FindPDF : public QObject { Q_OBJECT public: /// Used in a later stage (user interface, @see FindPDFUI); /// tells the system if ... - enum DownloadMode { - NoDownload = 0, ///< Ignore this result item (no PDF file downloading) - Download, ///< Download and store this PDF file in a user-specified location - URLonly ///< Keep only the URL of the PDF; this URL will be inserted in the bib entry + enum class DownloadMode { + No = 0, ///< Ignore this result item (no PDF file downloading) + PDFfile = 1, ///< Download and store this PDF file in a user-specified location + URLonly = 2 ///< Keep only the URL of the PDF; this URL will be inserted in the bib entry }; /// Structure to store data about every found PDF (potential search hit) typedef struct { QUrl url; ///< Where has this PDF been found? QString textPreview; ///< Text extracted from the PDF file QTemporaryFile *tempFilename; ///< Local temporary copy float relevance; /// Assessment of relevance (useful for sorting results) DownloadMode downloadMode; /// User's preference what to do with this hit (default is NoDownload) } ResultItem; explicit FindPDF(QObject *parent = nullptr); ~FindPDF() override; /** * Initiate a search for PDF files matching a given entry. * * @param entry entry to search PDF files for * @return @c true if the search could be started @c false if another search is still running */ bool search(const Entry &entry); /** * Once a search has been complete (signal @see finished), * this function allows to retrieve the collected results * @return @c After a search, list of results, @c before or during a search, an empty list */ QList<ResultItem> results(); signals: /** * A search initiated by @see search has been finished. */ void finished(); /** * Some update on the ongoing search. * Just of eye candy, can be safely ignored if no visualization of progress is possible. * * @param visitedPages how many web pages have been visited * @param runningJobs how many download/search operations are running in parallel * @param foundDocuments how many PDF files have been found */ void progress(int visitedPages, int runningJobs, int foundDocuments); public slots: /** * Abort any running downloads. */ void abort(); private slots: void downloadFinished(); private: class Private; Private *const d; }; #endif // KBIBTEX_NETWORKING_FINDPDF_H diff --git a/src/networking/urlchecker.cpp b/src/networking/urlchecker.cpp index cfb2b5cf..61ec634d 100644 --- a/src/networking/urlchecker.cpp +++ b/src/networking/urlchecker.cpp @@ -1,184 +1,184 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "urlchecker.h" #include <QTimer> #include <QSharedPointer> #include <QNetworkReply> #include <QRegularExpression> #include <QAtomicInteger> #include <Entry> #include <FileInfo> #include "internalnetworkaccessmanager.h" #include "logging_networking.h" class UrlChecker::Private { private: UrlChecker *p; public: QAtomicInteger<int> busyCounter; QSet<QUrl> urlsToCheck; Private(UrlChecker *parent) : p(parent) { /// nothing } void queueMoreOrFinish() { if (busyCounter.load() <= 0 && urlsToCheck.isEmpty()) { /// In case there are no running checks and the queue of URLs to check is empty, /// wait for a brief moment of time, then fire a 'finished' signal. QTimer::singleShot(100, p, [this]() { if (busyCounter.load() <= 0 && urlsToCheck.isEmpty()) QMetaObject::invokeMethod(p, "finished", Qt::DirectConnection, QGenericReturnArgument()); else /// It should not happen that when this timer is triggered the original condition is violated qCCritical(LOG_KBIBTEX_NETWORKING) << "This cannot happen:" << busyCounter.load() << urlsToCheck.count(); }); } else { /// Initiate as many checks as possible while (!urlsToCheck.isEmpty() && busyCounter.load() <= 4) checkNextUrl(); } } void checkNextUrl() { /// Immediately return if there are no URLs to check if (urlsToCheck.isEmpty()) return; /// Pop one URL from set of URLS to check auto firstUrlIt = urlsToCheck.begin(); const QUrl url = *firstUrlIt; urlsToCheck.erase(firstUrlIt); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); busyCounter.ref(); QObject::connect(reply, &QNetworkReply::finished, p, [this, reply]() { const QUrl url = reply->url(); if (reply->error() != QNetworkReply::NoError) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::NetworkError), Q_ARG(QString, reply->errorString())); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::NetworkError), Q_ARG(QString, reply->errorString())); qCWarning(LOG_KBIBTEX_NETWORKING) << "NetworkError:" << reply->errorString() << url.toDisplayString(); } else { const QByteArray data = reply->read(1024); if (data.isEmpty()) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UnknownError), Q_ARG(QString, QStringLiteral("No data received"))); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UnknownError), Q_ARG(QString, QStringLiteral("No data received"))); qCWarning(LOG_KBIBTEX_NETWORKING) << "UnknownError: No data received" << url.toDisplayString(); } else { const QString filename = url.fileName().toLower(); const bool filenameSuggestsHTML = filename.isEmpty() || filename.endsWith(QStringLiteral(".html")) || filename.endsWith(QStringLiteral(".htm")); const bool filenameSuggestsPDF = filename.endsWith(QStringLiteral(".pdf")); const bool filenameSuggestsPostScript = filename.endsWith(QStringLiteral(".ps")); const bool containsHTML = data.contains("<!DOCTYPE HTML") || data.contains("<html") || data.contains("<HTML") || data.contains("<body") || data.contains("<body"); const bool containsPDF = data.startsWith("%PDF"); const bool containsPostScript = data.startsWith("%!"); if (filenameSuggestsPDF && containsPDF) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UrlValid), Q_ARG(QString, QString())); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UrlValid), Q_ARG(QString, QString())); qCWarning(LOG_KBIBTEX_NETWORKING) << "UrlValid: Looks and smells like a PDF" << url.toDisplayString(); } else if (filenameSuggestsPostScript && containsPostScript) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UrlValid), Q_ARG(QString, QString())); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UrlValid), Q_ARG(QString, QString())); qCWarning(LOG_KBIBTEX_NETWORKING) << "UrlValid: Looks and smells like a PostScript" << url.toDisplayString(); } else if (containsHTML) { static const QRegularExpression error404(QStringLiteral("\\b404\\b")); const QRegularExpressionMatch error404match = error404.match(data); if (error404match.hasMatch()) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Error404), Q_ARG(QString, QStringLiteral("Got error 404"))); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::Error404), Q_ARG(QString, QStringLiteral("Got error 404"))); qCWarning(LOG_KBIBTEX_NETWORKING) << "Error404" << url.toDisplayString(); } else if (filenameSuggestsHTML) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UrlValid), Q_ARG(QString, QString())); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UrlValid), Q_ARG(QString, QString())); qCWarning(LOG_KBIBTEX_NETWORKING) << "UrlValid: Looks and smells like a HTML" << url.toDisplayString(); } else { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); qCWarning(LOG_KBIBTEX_NETWORKING) << "NotExpectedFileType (HTML): Filename's extension does not match content" << url.toDisplayString(); } } else if (filenameSuggestsPDF != containsPDF) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); qCWarning(LOG_KBIBTEX_NETWORKING) << "NotExpectedFileType (PDF): Filename's extension does not match content" << url.toDisplayString(); } else if (filenameSuggestsPostScript != containsPostScript) { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UnexpectedFileType), Q_ARG(QString, QStringLiteral("Filename's extension does not match content"))); qCWarning(LOG_KBIBTEX_NETWORKING) << "NotExpectedFileType (PostScript): Filename's extension does not match content" << url.toDisplayString(); } else { /// Instead of an 'emit' ... - QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::UrlValid), Q_ARG(QString, QString())); + QMetaObject::invokeMethod(p, "urlChecked", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QUrl, url), Q_ARG(UrlChecker::Status, UrlChecker::Status::UrlValid), Q_ARG(QString, QString())); qCWarning(LOG_KBIBTEX_NETWORKING) << "UrlValid: Cannot see any issued with this URL" << url.toDisplayString(); } } } busyCounter.deref(); queueMoreOrFinish(); }); } }; UrlChecker::UrlChecker(QObject *parent) : QObject(parent), d(new Private(this)) { /// nothing } UrlChecker::~UrlChecker() { delete d; } void UrlChecker::startChecking(const File &bibtexFile) { if (bibtexFile.count() < 1) { /// Nothing to do for empty bibliographies QTimer::singleShot(100, this, [this]() { emit finished(); }); return; } for (QSharedPointer<Element> element : bibtexFile) { /// Process only entries, not comments, preambles or macros QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (entry.isNull()) continue; /// Retrieve set of URLs per entry and add to set of URLS to be checked - const QSet<QUrl> thisEntryUrls = FileInfo::entryUrls(entry, bibtexFile.property(File::Url).toUrl(), FileInfo::TestExistenceNo); + const QSet<QUrl> thisEntryUrls = FileInfo::entryUrls(entry, bibtexFile.property(File::Url).toUrl(), FileInfo::TestExistence::No); for (const QUrl &u : thisEntryUrls) d->urlsToCheck.insert(u); ///< better? } if (d->urlsToCheck.isEmpty()) { /// No URLs identified in bibliography, so nothing to do QTimer::singleShot(100, this, [this]() { emit finished(); }); return; } d->queueMoreOrFinish(); } diff --git a/src/networking/urlchecker.h b/src/networking/urlchecker.h index ec90453f..c855ebfe 100644 --- a/src/networking/urlchecker.h +++ b/src/networking/urlchecker.h @@ -1,50 +1,50 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef URLCHECKER_H #define URLCHECKER_H #include <QObject> #include <File> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 class KBIBTEXNETWORKING_EXPORT UrlChecker : public QObject { Q_OBJECT public: - enum Status {UrlValid = 0, UnexpectedFileType, Error404, NetworkError, UnknownError}; + enum class Status {UrlValid = 0, UnexpectedFileType, Error404, NetworkError, UnknownError}; explicit UrlChecker(QObject *parent = nullptr); ~UrlChecker(); public slots: void startChecking(const File &bibtexFile); signals: void urlChecked(QUrl url, UrlChecker::Status status, QString msg); void finished(); private: class Private; Private *const d; }; #endif // URLCHECKER_H diff --git a/src/networking/zotero/api.cpp b/src/networking/zotero/api.cpp index e8633bd4..99d44139 100644 --- a/src/networking/zotero/api.cpp +++ b/src/networking/zotero/api.cpp @@ -1,107 +1,107 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "api.h" #include <QUrl> #include <QUrlQuery> #include <QDateTime> #include <QTimer> using namespace Zotero; class Zotero::API::Private { public: const QUrl apiBaseUrl; const int userOrGroupPrefix; const QString apiKey; QDateTime backoffElapseTime; Private(RequestScope requestScope, int prefix, const QString &_apiKey, Zotero::API *parent) - : apiBaseUrl(QUrl(QString(QStringLiteral("https://api.zotero.org/%1/%2")).arg(requestScope == GroupRequest ? QStringLiteral("groups") : QStringLiteral("users")).arg(prefix))), + : apiBaseUrl(QUrl(QString(QStringLiteral("https://api.zotero.org/%1/%2")).arg(requestScope == RequestScope::Group ? QStringLiteral("groups") : QStringLiteral("users")).arg(prefix))), userOrGroupPrefix(prefix), apiKey(_apiKey), backoffElapseTime(QDateTime::currentDateTime().addSecs(-5)) { Q_UNUSED(parent) } }; const int Zotero::API::limit = 45; API::API(RequestScope requestScope, int userOrGroupPrefix, const QString &apiKey, QObject *parent) : QObject(parent), d(new API::Private(requestScope, userOrGroupPrefix, apiKey, this)) { /// nothing } API::~API() { delete d; } void API::addLimitToUrl(QUrl &url) const { static const QString limitKey = QStringLiteral("limit"); QUrlQuery query(url); query.removeQueryItem(limitKey); query.addQueryItem(limitKey, QString::number(Zotero::API::limit)); url.setQuery(query); } QUrl API::baseUrl() const { return d->apiBaseUrl; } int API::userOrGroupPrefix() const { return d->userOrGroupPrefix; } QNetworkRequest API::request(const QUrl &url) const { QNetworkRequest request(url); request.setRawHeader("Zotero-API-Version", "3"); request.setRawHeader("Accept", "application/atom+xml"); request.setRawHeader("Authorization", QString(QStringLiteral("Bearer ")).append(d->apiKey).toLatin1().constData()); return request; } void API::startBackoff(int duration) { if (duration > 0 && !inBackoffMode()) { d->backoffElapseTime = QDateTime::currentDateTime().addSecs(duration + 1); emit backoffModeStart(); /// Use single-shot timer and functor to emit signal /// that backoff mode has finished QTimer::singleShot((duration + 1) * 1000, this, [ = ]() { emit backoffModeEnd(); }); } } bool API::inBackoffMode() const { return d->backoffElapseTime >= QDateTime::currentDateTime(); } qint64 API::backoffSecondsLeft() const { const qint64 diff = QDateTime::currentDateTime().secsTo(d->backoffElapseTime); if (diff < 0) return 0; else return diff; } diff --git a/src/networking/zotero/api.h b/src/networking/zotero/api.h index 99d052c7..9b0d1ad0 100644 --- a/src/networking/zotero/api.h +++ b/src/networking/zotero/api.h @@ -1,136 +1,136 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ZOTERO_API_H #define KBIBTEX_NETWORKING_ZOTERO_API_H #include <QObject> #include <QNetworkRequest> #include <QUrl> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 namespace Zotero { /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT API : public QObject { Q_OBJECT public: /** * Maximum number of items asked for in one request. * Multiple requests may be necessary to retrieve all data * the user asked for. */ static const int limit; /** * Scope of a request. Can be either a group request or a user request. */ - enum RequestScope { UserRequest, GroupRequest }; + enum class RequestScope { User, Group }; /** * Generate an API object encapsulating low-level interaction with Zotero. * @param requestScope determines if a group's or a user's data shall be queried * @param userOrGroupPrefix identifier for group or user (not username) * @param apiKey necessary API key to authenticate and to get authorization for requests on private data * @param parent used for Qt-internal operations */ explicit API(RequestScope requestScope, int userOrGroupPrefix, const QString &apiKey, QObject *parent = nullptr); ~API() override; /** * Add a limit parameter to a given Zotero URL. * @param url value of this parameter will be changed to contain the limit */ void addLimitToUrl(QUrl &url) const; /** * Base URL for all requests to Zotero. Contains user/group id, API key, ... * @return Basic URL which only needs to have a proper path set */ QUrl baseUrl() const; /** * @return Group or user prefix as specified in constructor. */ int userOrGroupPrefix() const; /** * Create a request to Zotero based on the provided URL, * having proper HTTP headers set. This request can be passed * to a QNetworkAccessManager * @param url URL to base the request on * @return Request to Zotero */ QNetworkRequest request(const QUrl &url) const; /** * A user of this API may receive a 'backoff' or 'retry-after' * notification from Zotero. In this case, this function has * to be called to notify this API object of the duration of * this backoff. Users can test if the API object is in 'backoff' * mode by calling @see inBackoffMode. * @param duration backoff's duration in seconds */ void startBackoff(int duration); /** * Due to high load at Zotero or due to excessive use by * the current user, further request to Zotero shall be * backed off for some time. Before a request is made * through this API, call this function to test if the * API object is in 'backoff' mode. * The backoff is not enforced by this API, but Zotero * may deny answering request during this time period. * @return true if backoff mode, false otherwise */ bool inBackoffMode() const; /** * Computes the number of seconds still left for backoff * mode (>=0). If the API is not in backoff mode, 0 will * be returned. * @return a non-negative number of seconds left in backoff mode */ qint64 backoffSecondsLeft() const; signals: /** * Signal gets emitted when backoff mode is entered. */ void backoffModeStart(); /** * Signal gets emitted when backoffmode is left. */ void backoffModeEnd(); private: class Private; Private *const d; }; } // end of namespace Zotero #endif // KBIBTEX_NETWORKING_ZOTERO_API_H diff --git a/src/networking/zotero/tagmodel.h b/src/networking/zotero/tagmodel.h index d7399f84..9105dc2a 100644 --- a/src/networking/zotero/tagmodel.h +++ b/src/networking/zotero/tagmodel.h @@ -1,60 +1,63 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H #define KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H #include <QAbstractItemModel> #include <QHash> #include <QVector> #include <QSet> #ifdef HAVE_KF5 #include "kbibtexnetworking_export.h" #endif // HAVE_KF5 namespace Zotero { class Tags; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXNETWORKING_EXPORT TagModel : public QAbstractItemModel { Q_OBJECT public: - enum Roles { TagRole = Qt::UserRole + 6685, TagCountRole = Qt::UserRole + 6686 }; + enum TagModelRoles { + TagRole = Qt::UserRole + 6685, + TagCountRole = Qt::UserRole + 6686 + }; explicit TagModel(Zotero::Tags *tags, QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &) const override; QModelIndex parent(const QModelIndex &) const override; int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; private: class Private; Private *const d; }; } // end of namespace Zotero #endif // KBIBTEX_NETWORKING_ZOTERO_TAGMODEL_H diff --git a/src/parts/part.cpp b/src/parts/part.cpp index 1779a5b7..0175a481 100644 --- a/src/parts/part.cpp +++ b/src/parts/part.cpp @@ -1,1046 +1,1046 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "part.h" #include <QLabel> #include <QAction> #include <QFile> #include <QFileInfo> #include <QMenu> #include <QApplication> #include <QLayout> #include <QKeyEvent> #include <QMimeType> #include <QPointer> #include <QFileSystemWatcher> #include <QFileDialog> #include <QDialog> #include <QDialogButtonBox> #include <QPushButton> #include <QTemporaryFile> #include <QTimer> #include <QStandardPaths> #include <KMessageBox> // FIXME deprecated #include <KLocalizedString> #include <KActionCollection> #include <KStandardAction> #include <KActionMenu> #include <KSelectAction> #include <KToggleAction> #include <KRun> #include <KPluginFactory> #include <KIO/StatJob> #include <KIO/CopyJob> #include <KIO/Job> #include <KJobWidgets> #include <kio_version.h> #include <Preferences> #include <File> #include <Macro> #include <Preamble> #include <Comment> #include <FileInfo> #include <FileExporterBibTeXOutput> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <FileImporterRIS> #include <FileImporterBibUtils> #include <FileExporterRIS> #include <FileExporterBibUtils> #include <FileImporterPDF> #include <FileExporterPS> #include <FileExporterPDF> #include <FileExporterRTF> #include <FileExporterBibTeX2HTML> #include <FileExporterXML> #include <FileExporterXSLT> #include <models/FileModel> #include <IdSuggestions> #include <LyX> #include <UrlChecker> #include <widgets/FileSettingsWidget> #include <widgets/FilterBar> #include <element/FindPDFUI> #include <file/FileView> #include <file/FindDuplicatesUI> #include <file/Clipboard> #include <preferences/SettingsColorLabelWidget> #include <preferences/SettingsFileExporterPDFPSWidget> #include <ValueListModel> #include "logging_part.h" static const char RCFileName[] = "kbibtexpartui.rc"; class KBibTeXPart::KBibTeXPartPrivate { private: KBibTeXPart *p; /** * Modifies a given URL to become a "backup" filename/URL. * A backup level or 0 or less does not modify the URL. * A backup level of 1 appends a '~' (tilde) to the URL's filename. * A backup level of 2 or more appends '~N', where N is the level. * The provided URL will be modified in the process. It is assumed * that the URL is not yet a "backup URL". */ void constructBackupUrl(const int level, QUrl &url) const { if (level <= 0) /// No modification return; else if (level == 1) /// Simply append '~' to the URL's filename url.setPath(url.path() + QStringLiteral("~")); else /// Append '~' followed by a number to the filename url.setPath(url.path() + QString(QStringLiteral("~%1")).arg(level)); } public: File *bibTeXFile; PartWidget *partWidget; FileModel *model; SortFilterFileModel *sortFilterProxyModel; QAction *editCutAction, *editDeleteAction, *editCopyAction, *editPasteAction, *editCopyReferencesAction, *elementEditAction, *elementViewDocumentAction, *fileSaveAction, *elementFindPDFAction, *entryApplyDefaultFormatString; QMenu *viewDocumentMenu; bool isSaveAsOperation; LyX *lyx; FindDuplicatesUI *findDuplicatesUI; ColorLabelContextMenu *colorLabelContextMenu; QAction *colorLabelContextMenuAction; QFileSystemWatcher fileSystemWatcher; KBibTeXPartPrivate(QWidget *parentWidget, KBibTeXPart *parent) : p(parent), bibTeXFile(nullptr), model(nullptr), sortFilterProxyModel(nullptr), viewDocumentMenu(new QMenu(i18n("View Document"), parent->widget())), isSaveAsOperation(false), fileSystemWatcher(p) { connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, p, &KBibTeXPart::fileExternallyChange); partWidget = new PartWidget(parentWidget); partWidget->fileView()->setReadOnly(!p->isReadWrite()); connect(partWidget->fileView(), &FileView::modified, p, &KBibTeXPart::setModified); setupActions(); } ~KBibTeXPartPrivate() { delete bibTeXFile; delete model; delete viewDocumentMenu; delete findDuplicatesUI; } void setupActions() { /// "Save" action fileSaveAction = p->actionCollection()->addAction(KStandardAction::Save); connect(fileSaveAction, &QAction::triggered, p, &KBibTeXPart::documentSave); fileSaveAction->setEnabled(false); QAction *action = p->actionCollection()->addAction(KStandardAction::SaveAs); connect(action, &QAction::triggered, p, &KBibTeXPart::documentSaveAs); /// "Save copy as" action QAction *saveCopyAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Copy As..."), p); p->actionCollection()->addAction(QStringLiteral("file_save_copy_as"), saveCopyAsAction); connect(saveCopyAsAction, &QAction::triggered, p, &KBibTeXPart::documentSaveCopyAs); /// Filter bar widget QAction *filterWidgetAction = new QAction(i18n("Filter"), p); p->actionCollection()->addAction(QStringLiteral("toolbar_filter_widget"), filterWidgetAction); filterWidgetAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); p->actionCollection()->setDefaultShortcut(filterWidgetAction, Qt::CTRL + Qt::Key_F); connect(filterWidgetAction, &QAction::triggered, partWidget->filterBar(), static_cast<void(QWidget::*)()>(&QWidget::setFocus)); partWidget->filterBar()->setPlaceholderText(i18n("Filter bibliographic entries (%1)", filterWidgetAction->shortcut().toString())); /// Actions for creating new elements (entries, macros, ...) KActionMenu *newElementAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New element"), p); p->actionCollection()->addAction(QStringLiteral("element_new"), newElementAction); QMenu *newElementMenu = new QMenu(newElementAction->text(), p->widget()); newElementAction->setMenu(newElementMenu); connect(newElementAction, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newEntry = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New entry"), newElementAction); newElementMenu->addAction(newEntry); p->actionCollection()->setDefaultShortcut(newEntry, Qt::CTRL + Qt::SHIFT + Qt::Key_N); connect(newEntry, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newComment = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New comment"), newElementAction); newElementMenu->addAction(newComment); connect(newComment, &QAction::triggered, p, &KBibTeXPart::newCommentTriggered); QAction *newMacro = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New macro"), newElementAction); newElementMenu->addAction(newMacro); connect(newMacro, &QAction::triggered, p, &KBibTeXPart::newMacroTriggered); QAction *newPreamble = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New preamble"), newElementAction); newElementMenu->addAction(newPreamble); connect(newPreamble, &QAction::triggered, p, &KBibTeXPart::newPreambleTriggered); /// Action to edit an element elementEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Element"), p); p->actionCollection()->addAction(QStringLiteral("element_edit"), elementEditAction); p->actionCollection()->setDefaultShortcut(elementEditAction, Qt::CTRL + Qt::Key_E); connect(elementEditAction, &QAction::triggered, partWidget->fileView(), &FileView::editCurrentElement); /// Action to view the document associated to the current element elementViewDocumentAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View Document"), p); p->actionCollection()->addAction(QStringLiteral("element_viewdocument"), elementViewDocumentAction); p->actionCollection()->setDefaultShortcut(elementViewDocumentAction, Qt::CTRL + Qt::Key_D); connect(elementViewDocumentAction, &QAction::triggered, p, &KBibTeXPart::elementViewDocument); /// Action to find a PDF matching the current element elementFindPDFAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("Find PDF..."), p); p->actionCollection()->addAction(QStringLiteral("element_findpdf"), elementFindPDFAction); connect(elementFindPDFAction, &QAction::triggered, p, &KBibTeXPart::elementFindPDF); /// Action to reformat the selected elements' ids entryApplyDefaultFormatString = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Format entry ids"), p); p->actionCollection()->addAction(QStringLiteral("entry_applydefaultformatstring"), entryApplyDefaultFormatString); connect(entryApplyDefaultFormatString, &QAction::triggered, p, &KBibTeXPart::applyDefaultFormatString); /// Clipboard object, required for various copy&paste operations Clipboard *clipboard = new Clipboard(partWidget->fileView()); /// Actions to cut and copy selected elements as BibTeX code editCutAction = p->actionCollection()->addAction(KStandardAction::Cut, clipboard, SLOT(cut())); editCopyAction = p->actionCollection()->addAction(KStandardAction::Copy, clipboard, SLOT(copy())); /// Action to copy references, e.g. '\cite{fordfulkerson1959}' editCopyReferencesAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy References"), p); p->actionCollection()->setDefaultShortcut(editCopyReferencesAction, Qt::CTRL + Qt::SHIFT + Qt::Key_C); p->actionCollection()->addAction(QStringLiteral("edit_copy_references"), editCopyReferencesAction); connect(editCopyReferencesAction, &QAction::triggered, clipboard, &Clipboard::copyReferences); /// Action to paste BibTeX code editPasteAction = p->actionCollection()->addAction(KStandardAction::Paste, clipboard, SLOT(paste())); /// Action to delete selected rows/elements editDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete"), p); p->actionCollection()->setDefaultShortcut(editDeleteAction, Qt::Key_Delete); p->actionCollection()->addAction(QStringLiteral("edit_delete"), editDeleteAction); connect(editDeleteAction, &QAction::triggered, partWidget->fileView(), &FileView::selectionDelete); /// Build context menu for central BibTeX file view partWidget->fileView()->setContextMenuPolicy(Qt::ActionsContextMenu); ///< context menu is based on actions partWidget->fileView()->addAction(elementEditAction); partWidget->fileView()->addAction(elementViewDocumentAction); QAction *separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(editCutAction); partWidget->fileView()->addAction(editCopyAction); partWidget->fileView()->addAction(editCopyReferencesAction); partWidget->fileView()->addAction(editPasteAction); partWidget->fileView()->addAction(editDeleteAction); separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(elementFindPDFAction); partWidget->fileView()->addAction(entryApplyDefaultFormatString); colorLabelContextMenu = new ColorLabelContextMenu(partWidget->fileView()); colorLabelContextMenuAction = p->actionCollection()->addAction(QStringLiteral("entry_colorlabel"), colorLabelContextMenu->menuAction()); findDuplicatesUI = new FindDuplicatesUI(p, partWidget->fileView()); lyx = new LyX(p, partWidget->fileView()); connect(partWidget->fileView(), &FileView::selectedElementsChanged, p, &KBibTeXPart::updateActions); connect(partWidget->fileView(), &FileView::currentElementChanged, p, &KBibTeXPart::updateActions); } FileImporter *fileImporterFactory(const QUrl &url) { QString ending = url.path().toLower(); const auto pos = ending.lastIndexOf(QStringLiteral(".")); ending = ending.mid(pos + 1); if (ending == QStringLiteral("pdf")) { return new FileImporterPDF(p); } else if (ending == QStringLiteral("ris")) { return new FileImporterRIS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileImporterBibUtils *fileImporterBibUtils = new FileImporterBibUtils(p); - fileImporterBibUtils->setFormat(BibUtils::ISI); + fileImporterBibUtils->setFormat(BibUtils::Format::ISI); return fileImporterBibUtils; } else { FileImporterBibTeX *fileImporterBibTeX = new FileImporterBibTeX(p); - fileImporterBibTeX->setCommentHandling(FileImporterBibTeX::KeepComments); + fileImporterBibTeX->setCommentHandling(FileImporterBibTeX::CommentHandling::Keep); return fileImporterBibTeX; } } FileExporter *fileExporterFactory(const QString &ending) { if (ending == QStringLiteral("html")) { return new FileExporterHTML(p); } else if (ending == QStringLiteral("xml")) { return new FileExporterXML(p); } else if (ending == QStringLiteral("ris")) { return new FileExporterRIS(p); } else if (ending == QStringLiteral("pdf")) { return new FileExporterPDF(p); } else if (ending == QStringLiteral("ps")) { return new FileExporterPS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileExporterBibUtils *fileExporterBibUtils = new FileExporterBibUtils(p); - fileExporterBibUtils->setFormat(BibUtils::ISI); + fileExporterBibUtils->setFormat(BibUtils::Format::ISI); return fileExporterBibUtils; } else if (ending == QStringLiteral("rtf")) { return new FileExporterRTF(p); } else if (ending == QStringLiteral("html") || ending == QStringLiteral("htm")) { return new FileExporterBibTeX2HTML(p); } else if (ending == QStringLiteral("bbl")) { - return new FileExporterBibTeXOutput(FileExporterBibTeXOutput::BibTeXBlockList, p); + return new FileExporterBibTeXOutput(FileExporterBibTeXOutput::OutputType::BibTeXBlockList, p); } else { return new FileExporterBibTeX(p); } } QString findUnusedId() { int i = 1; while (true) { QString result = i18n("New%1", i); if (!bibTeXFile->containsKey(result)) return result; ++i; } } void initializeNew() { bibTeXFile = new File(); model = new FileModel(); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); } bool openFile(const QUrl &url, const QString &localFilePath) { p->setObjectName("KBibTeXPart::KBibTeXPart for " + url.toDisplayString() + " aka " + localFilePath); qApp->setOverrideCursor(Qt::WaitCursor); if (bibTeXFile != nullptr) { const QUrl oldUrl = bibTeXFile->property(File::Url, QUrl()).toUrl(); if (oldUrl.isValid() && oldUrl.isLocalFile()) { const QString path = oldUrl.toLocalFile(); if (!path.isEmpty()) fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching"; } delete bibTeXFile; bibTeXFile = nullptr; } QFile inputfile(localFilePath); if (!inputfile.open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_PART) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } FileImporter *importer = fileImporterFactory(url); importer->showImportDialog(p->widget()); bibTeXFile = importer->load(&inputfile); inputfile.close(); delete importer; if (bibTeXFile == nullptr) { qCWarning(LOG_KBIBTEX_PART) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } bibTeXFile->setProperty(File::Url, QUrl(url)); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); if (url.isLocalFile()) fileSystemWatcher.addPath(url.toLocalFile()); qApp->restoreOverrideCursor(); return true; } void makeBackup(const QUrl &url) const { /// Fetch settings from configuration const int numberOfBackups = Preferences::instance().numberOfBackups(); /// Stop right here if no backup is requested - if (Preferences::instance().backupScope() == Preferences::NoBackup) + if (Preferences::instance().backupScope() == Preferences::BackupScope::None) return; /// For non-local files, proceed only if backups to remote storage is allowed - if (Preferences::instance().backupScope() != Preferences::BothLocalAndRemote && !url.isLocalFile()) + if (Preferences::instance().backupScope() != Preferences::BackupScope::BothLocalAndRemote && !url.isLocalFile()) return; /// Do not make backup copies if destination file does not exist yet KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); statJob->exec(); if (statJob->error() == KIO::ERR_DOES_NOT_EXIST) return; else if (statJob->error() != KIO::Job::NoError) { /// Something else went wrong, quit with error qCWarning(LOG_KBIBTEX_PART) << "Probing" << url.toDisplayString() << "failed:" << statJob->errorString(); return; } bool copySucceeded = true; /// Copy e.g. test.bib~ to test.bib~2, test.bib to test.bib~ etc. for (int level = numberOfBackups; copySucceeded && level >= 1; --level) { QUrl newerBackupUrl = url; constructBackupUrl(level - 1, newerBackupUrl); QUrl olderBackupUrl = url; constructBackupUrl(level, olderBackupUrl); statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); if (statJob->exec() && statJob->error() == KIO::Job::NoError) { KIO::CopyJob *moveJob = nullptr; ///< guaranteed to be initialized in either branch of the following code /** * The following 'if' block is necessary to handle the * following situation: User opens, modifies, and saves * file /tmp/b/bbb.bib which is actually a symlink to * file /tmp/a/aaa.bib. Now a 'move' operation like the * implicit 'else' section below does, would move /tmp/b/bbb.bib * to become /tmp/b/bbb.bib~ still pointing to /tmp/a/aaa.bib. * Then, the save operation would create a new file /tmp/b/bbb.bib * without any symbolic linking to /tmp/a/aaa.bib. * The following code therefore checks if /tmp/b/bbb.bib is * to be copied/moved to /tmp/b/bbb.bib~ and /tmp/b/bbb.bib * is a local file and /tmp/b/bbb.bib is a symbolic link to * another file. Then /tmp/b/bbb.bib is resolved to the real * file /tmp/a/aaa.bib which is then copied into plain file * /tmp/b/bbb.bib~. The save function (outside of this function's * scope) will then see that /tmp/b/bbb.bib is a symbolic link, * resolve this symlink to /tmp/a/aaa.bib, and then write * all changes to /tmp/a/aaa.bib keeping /tmp/b/bbb.bib a * link to. */ if (level == 1 && newerBackupUrl.isLocalFile() /** for level==1, this is actually the current file*/) { QFileInfo newerBackupFileInfo(newerBackupUrl.toLocalFile()); if (newerBackupFileInfo.isSymLink()) { while (newerBackupFileInfo.isSymLink()) { newerBackupUrl = QUrl::fromLocalFile(newerBackupFileInfo.symLinkTarget()); newerBackupFileInfo = QFileInfo(newerBackupUrl.toLocalFile()); } moveJob = KIO::copy(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); } } if (moveJob == nullptr) ///< implicit 'else' section, see longer comment above moveJob = KIO::move(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(moveJob, p->widget()); copySucceeded = moveJob->exec(); } } if (!copySucceeded) KMessageBox::error(p->widget(), i18n("Could not create backup copies of document '%1'.", url.url(QUrl::PreferLocalFile)), i18n("Backup copies")); } QUrl getSaveFilename(bool mustBeImportable = true) { QString startDir = p->url().isValid() ? p->url().path() : QString(); QString supportedMimeTypes = QStringLiteral("text/x-bibtex text/x-research-info-systems"); if (BibUtils::available()) supportedMimeTypes += QStringLiteral(" application/x-isi-export-format application/x-endnote-refer"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("pdflatex")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/pdf"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("dvips")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/postscript"); if (!mustBeImportable) supportedMimeTypes += QStringLiteral(" text/html"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("latex2rtf")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/rtf"); QPointer<QFileDialog> saveDlg = new QFileDialog(p->widget(), i18n("Save file") /* TODO better text */, startDir, supportedMimeTypes); /// Setting list of mime types for the second time, /// essentially calling this function only to set the "default mime type" parameter saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), QString::SkipEmptyParts)); /// Setting the dialog into "Saving" mode make the "add extension" checkbox available saveDlg->setAcceptMode(QFileDialog::AcceptSave); saveDlg->setDefaultSuffix(QStringLiteral("bib")); saveDlg->setFileMode(QFileDialog::AnyFile); if (saveDlg->exec() != QDialog::Accepted) /// User cancelled saving operation, return invalid filename/URL return QUrl(); const QList<QUrl> selectedUrls = saveDlg->selectedUrls(); delete saveDlg; return selectedUrls.isEmpty() ? QUrl() : selectedUrls.first(); } FileExporter *saveFileExporter(const QString &ending) { FileExporter *exporter = fileExporterFactory(ending); if (isSaveAsOperation) { /// only show export dialog at SaveAs or SaveCopyAs operations FileExporterToolchain *fet = nullptr; if (FileExporterBibTeX::isFileExporterBibTeX(*exporter)) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("BibTeX File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); FileSettingsWidget *settingsWidget = new FileSettingsWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToLoadedProperties); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); settingsWidget->loadProperties(bibTeXFile); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveProperties(bibTeXFile); delete dlg; } else if ((fet = qobject_cast<FileExporterToolchain *>(exporter)) != nullptr) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("PDF/PostScript File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); SettingsFileExporterPDFPSWidget *settingsWidget = new SettingsFileExporterPDFPSWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::loadState); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveState(); delete dlg; } } return exporter; } bool saveFile(QFile &file, FileExporter *exporter, QStringList *errorLog = nullptr) { SortFilterFileModel *model = qobject_cast<SortFilterFileModel *>(partWidget->fileView()->model()); Q_ASSERT_X(model != nullptr, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel *model from editor->model() is invalid"); return exporter->save(&file, model->fileSourceModel()->bibliographyFile(), errorLog); } bool saveFile(const QUrl &url) { bool result = false; Q_ASSERT_X(url.isValid(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url)", "url must be valid"); /// Extract filename extension (e.g. 'bib') to determine which FileExporter to use static const QRegularExpression suffixRegExp(QStringLiteral("\\.([^.]{1,4})$")); const QRegularExpressionMatch suffixRegExpMatch = suffixRegExp.match(url.fileName()); const QString ending = suffixRegExpMatch.hasMatch() ? suffixRegExpMatch.captured(1) : QStringLiteral("bib"); FileExporter *exporter = saveFileExporter(ending); /// String list to collect error message from FileExporer QStringList errorLog; qApp->setOverrideCursor(Qt::WaitCursor); if (url.isLocalFile()) { /// Take precautions for local files QFileInfo fileInfo(url.toLocalFile()); /// Do not overwrite symbolic link, but linked file instead QString filename = fileInfo.absoluteFilePath(); while (fileInfo.isSymLink()) { filename = fileInfo.symLinkTarget(); fileInfo = QFileInfo(filename); } if (!fileInfo.exists() || fileInfo.isWritable()) { /// Make backup before overwriting target destination, intentionally /// using the provided filename, not the resolved symlink makeBackup(url); QFile file(filename); if (file.open(QIODevice::WriteOnly)) { result = saveFile(file, exporter, &errorLog); file.close(); } } } else { /// URL points to a remote location /// Configure and open temporary file QTemporaryFile temporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_savefile_XXXXXX") + ending); temporaryFile.setAutoRemove(true); if (temporaryFile.open()) { result = saveFile(temporaryFile, exporter, &errorLog); /// Close/flush temporary file temporaryFile.close(); if (result) { /// Make backup before overwriting target destination makeBackup(url); KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(temporaryFile.fileName()), url, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(copyJob, p->widget()); result &= copyJob->exec() && copyJob->error() == KIO::Job::NoError; } } } qApp->restoreOverrideCursor(); delete exporter; if (!result) { QString msg = i18n("Saving the bibliography to file '%1' failed.", url.toDisplayString()); if (errorLog.isEmpty()) KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed")); else { msg += QLatin1String("\n\n"); msg += i18n("The following output was generated by the export filter:"); KMessageBox::errorList(p->widget(), msg, errorLog, i18n("Saving bibliography failed")); } } return result; } /** * Builds or resets the menu with local and remote * references (URLs, files) of an entry. * * @return Number of known references */ int updateViewDocumentMenu() { viewDocumentMenu->clear(); int result = 0; ///< Initially, no references are known File *bibliographyFile = partWidget != nullptr && partWidget->fileView() != nullptr && partWidget->fileView()->fileModel() != nullptr ? partWidget->fileView()->fileModel()->bibliographyFile() : nullptr; if (bibliographyFile == nullptr) return result; /// Retrieve Entry object of currently selected line /// in main list view QSharedPointer<const Entry> entry = partWidget->fileView()->currentElement().dynamicCast<const Entry>(); /// Test and continue if there was an Entry to retrieve if (!entry.isNull()) { /// Get list of URLs associated with this entry - const auto urlList = FileInfo::entryUrls(entry, bibliographyFile->property(File::Url).toUrl(), FileInfo::TestExistenceYes); + const auto urlList = FileInfo::entryUrls(entry, bibliographyFile->property(File::Url).toUrl(), FileInfo::TestExistence::Yes); if (!urlList.isEmpty()) { /// Memorize first action, necessary to set menu title QAction *firstAction = nullptr; /// First iteration: local references only for (const QUrl &url : urlList) { /// First iteration: local references only if (!url.isLocalFile()) continue; ///< skip remote URLs /// Build a nice menu item (label, icon, ...) const QFileInfo fi(url.toLocalFile()); const QString label = QString(QStringLiteral("%1 [%2]")).arg(fi.fileName(), fi.absolutePath()); QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), label, p); action->setToolTip(fi.absoluteFilePath()); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, fi]() { elementViewDocumentMenu(QUrl::fromLocalFile(fi.absoluteFilePath())); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// local URLs (i.e. local files) and firstAction /// is the first one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Local Files")); } firstAction = nullptr; /// Now the first remote action is to be memorized /// Second iteration: remote references only for (const QUrl &url : urlList) { if (url.isLocalFile()) continue; ///< skip local files /// Build a nice menu item (label, icon, ...) const QString prettyUrl = url.toDisplayString(); QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), prettyUrl, p); action->setToolTip(prettyUrl); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, url]() { elementViewDocumentMenu(url); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// some remote URLs and firstAction is the first /// one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Remote Files")); } result = urlList.count(); } } return result; } void readConfiguration() { disconnect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); disconnect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); switch (Preferences::instance().fileViewDoubleClickAction()) { - case Preferences::ActionOpenEditor: + case Preferences::FileViewDoubleClickAction::OpenEditor: connect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); break; - case Preferences::ActionViewDocument: + case Preferences::FileViewDoubleClickAction::ViewDocument: connect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); break; } } void elementViewDocumentMenu(const QUrl &url) { const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p->widget(), KRun::RunFlags()); } }; KBibTeXPart::KBibTeXPart(QWidget *parentWidget, QObject *parent, const KAboutData &componentData) : KParts::ReadWritePart(parent), d(new KBibTeXPartPrivate(parentWidget, this)) { setComponentData(componentData); setWidget(d->partWidget); updateActions(); d->initializeNew(); setXMLFile(RCFileName); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); d->readConfiguration(); setModified(false); } KBibTeXPart::~KBibTeXPart() { delete d; } void KBibTeXPart::setModified(bool modified) { KParts::ReadWritePart::setModified(modified); d->fileSaveAction->setEnabled(modified); } void KBibTeXPart::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) d->readConfiguration(); } bool KBibTeXPart::saveFile() { Q_ASSERT_X(isReadWrite(), "bool KBibTeXPart::saveFile()", "Trying to save although document is in read-only mode"); if (!url().isValid()) return documentSaveAs(); /// If the current file is "watchable" (i.e. a local file), /// memorize local filename for future reference const QString watchableFilename = url().isValid() && url().isLocalFile() ? url().toLocalFile() : QString(); /// Stop watching local file that will be written to if (!watchableFilename.isEmpty()) d->fileSystemWatcher.removePath(watchableFilename); else qCWarning(LOG_KBIBTEX_PART) << "watchableFilename is Empty"; const bool saveOperationSuccess = d->saveFile(url()); if (!watchableFilename.isEmpty()) { /// Continue watching a local file after write operation, but do /// so only after a short delay. The delay is necessary in some /// situations as observed in KDE bug report 396343 where the /// DropBox client seemingly touched the file right after saving /// from within KBibTeX, triggering KBibTeX to show a 'reload' /// message box. QTimer::singleShot(500, this, [this, watchableFilename]() { d->fileSystemWatcher.addPath(watchableFilename); }); } else qCWarning(LOG_KBIBTEX_PART) << "watchableFilename is Empty"; if (!saveOperationSuccess) { KMessageBox::error(widget(), i18n("The document could not be saved, as it was not possible to write to '%1'.\n\nCheck that you have write access to this file or that enough disk space is available.", url().toDisplayString())); return false; } return true; } bool KBibTeXPart::documentSave() { d->isSaveAsOperation = false; if (!isReadWrite()) return documentSaveCopyAs(); else if (!url().isValid()) return documentSaveAs(); else return KParts::ReadWritePart::save(); } bool KBibTeXPart::documentSaveAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(); if (!newUrl.isValid()) return false; /// Remove old URL from file system watcher if (url().isValid() && url().isLocalFile()) { const QString path = url().toLocalFile(); if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching"; } else qCWarning(LOG_KBIBTEX_PART) << "Not removing" << url().url(QUrl::PreferLocalFile) << "from fileSystemWatcher"; // TODO how does SaveAs dialog know which mime types to support? if (KParts::ReadWritePart::saveAs(newUrl)) { // FIXME d->model->bibliographyFile()->setProperty(File::Url, newUrl); return true; } else return false; } bool KBibTeXPart::documentSaveCopyAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(false); if (!newUrl.isValid() || newUrl == url()) return false; /// difference from KParts::ReadWritePart::saveAs: /// current document's URL won't be changed return d->saveFile(newUrl); } void KBibTeXPart::elementViewDocument() { QUrl url; const QList<QAction *> actionList = d->viewDocumentMenu->actions(); /// Go through all actions (i.e. document URLs) for this element for (const QAction *action : actionList) { /// Make URL from action's data ... QUrl tmpUrl = QUrl(action->data().toString()); /// ... but skip this action if the URL is invalid if (!tmpUrl.isValid()) continue; if (tmpUrl.isLocalFile()) { /// If action's URL points to local file, /// keep it and stop search for document url = tmpUrl; break; } else if (!url.isValid()) /// First valid URL found, keep it /// URL is not local, so it may get overwritten by another URL url = tmpUrl; } /// Open selected URL if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags()); } } void KBibTeXPart::elementFindPDF() { QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); if (mil.count() == 1) { QSharedPointer<Entry> entry = d->partWidget->fileView()->fileModel()->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(*mil.constBegin()).row()).dynamicCast<Entry>(); if (!entry.isNull()) FindPDFUI::interactiveFindPDF(*entry, *d->bibTeXFile, widget()); } } void KBibTeXPart::applyDefaultFormatString() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool documentModified = false; const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); for (const QModelIndex &index : mil) { QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) { static IdSuggestions idSuggestions; bool success = idSuggestions.applyDefaultFormatId(*entry.data()); documentModified |= success; if (!success) { KMessageBox::information(widget(), i18n("Cannot apply default formatting for entry ids: No default format specified."), i18n("Cannot Apply Default Formatting")); break; } } } if (documentModified) d->partWidget->fileView()->externalModification(); } bool KBibTeXPart::openFile() { const bool success = d->openFile(url(), localFilePath()); emit completed(); return success; } void KBibTeXPart::newEntryTriggered() { QSharedPointer<Entry> newEntry = QSharedPointer<Entry>(new Entry(QStringLiteral("Article"), d->findUnusedId())); d->model->insertRow(newEntry, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newEntry); if (d->partWidget->fileView()->editElement(newEntry)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newMacroTriggered() { QSharedPointer<Macro> newMacro = QSharedPointer<Macro>(new Macro(d->findUnusedId())); d->model->insertRow(newMacro, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newMacro); if (d->partWidget->fileView()->editElement(newMacro)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newPreambleTriggered() { QSharedPointer<Preamble> newPreamble = QSharedPointer<Preamble>(new Preamble()); d->model->insertRow(newPreamble, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newPreamble); if (d->partWidget->fileView()->editElement(newPreamble)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newCommentTriggered() { QSharedPointer<Comment> newComment = QSharedPointer<Comment>(new Comment()); d->model->insertRow(newComment, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newComment); if (d->partWidget->fileView()->editElement(newComment)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::updateActions() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool emptySelection = d->partWidget->fileView()->selectedElements().isEmpty(); d->elementEditAction->setEnabled(!emptySelection); d->editCopyAction->setEnabled(!emptySelection); d->editCopyReferencesAction->setEnabled(!emptySelection); d->editCutAction->setEnabled(!emptySelection && isReadWrite()); d->editPasteAction->setEnabled(isReadWrite()); d->editDeleteAction->setEnabled(!emptySelection && isReadWrite()); d->elementFindPDFAction->setEnabled(!emptySelection && isReadWrite()); d->entryApplyDefaultFormatString->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenu->menuAction()->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenuAction->setEnabled(!emptySelection && isReadWrite()); int numDocumentsToView = d->updateViewDocumentMenu(); /// enable menu item only if there is at least one document to view d->elementViewDocumentAction->setEnabled(!emptySelection && numDocumentsToView > 0); /// activate sub-menu only if there are at least two documents to view d->elementViewDocumentAction->setMenu(numDocumentsToView > 1 ? d->viewDocumentMenu : nullptr); d->elementViewDocumentAction->setToolTip(numDocumentsToView == 1 ? (*d->viewDocumentMenu->actions().constBegin())->text() : QString()); /// update list of references which can be sent to LyX QStringList references; if (d->partWidget->fileView()->selectionModel() != nullptr) { const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); references.reserve(mil.size()); for (const QModelIndex &index : mil) { const QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) references << entry->id(); } } d->lyx->setReferences(references); } void KBibTeXPart::fileExternallyChange(const QString &path) { /// Should never happen: triggering this slot for non-local or invalid URLs if (!url().isValid() || !url().isLocalFile()) return; /// Should never happen: triggering this slot for filenames not being the opened file if (path != url().toLocalFile()) { qCWarning(LOG_KBIBTEX_PART) << "Got file modification warning for wrong file: " << path << "!=" << url().toLocalFile(); return; } /// Stop watching file while asking for user interaction if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching"; if (KMessageBox::warningContinueCancel(widget(), i18n("The file '%1' has changed on disk.\n\nReload file or ignore changes on disk?", path), i18n("File changed externally"), KGuiItem(i18n("Reload file"), QIcon::fromTheme(QStringLiteral("edit-redo"))), KGuiItem(i18n("Ignore on-disk changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Continue) { d->openFile(QUrl::fromLocalFile(path), path); /// No explicit call to QFileSystemWatcher.addPath(...) necessary, /// openFile(...) has done that already } else { /// Even if the user did not request reloaded the file, /// still resume watching file for future external changes if (!path.isEmpty()) d->fileSystemWatcher.addPath(path); else qCWarning(LOG_KBIBTEX_PART) << "path is Empty"; } } #include "part.moc" diff --git a/src/processing/checkbibtex.cpp b/src/processing/checkbibtex.cpp index 5c57cec0..e2b4d0d2 100644 --- a/src/processing/checkbibtex.cpp +++ b/src/processing/checkbibtex.cpp @@ -1,151 +1,151 @@ /*************************************************************************** - * Copyright (C) 2004-2017 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "checkbibtex.h" #include <QApplication> #include <QBuffer> #include <QTextStream> #include <QRegularExpression> #include <KLocalizedString> #include <KMessageBox> #include <File> #include <Entry> #include <Element> #include <Macro> #include <FileExporterBibTeXOutput> CheckBibTeX::CheckBibTeXResult CheckBibTeX::checkBibTeX(QSharedPointer<Element> &element, const File *file, QWidget *parent) { /// only entries are supported, no macros, preambles, ... QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (entry.isNull()) - return InvalidData; + return CheckBibTeXResult::InvalidData; else return checkBibTeX(entry, file, parent); } CheckBibTeX::CheckBibTeXResult CheckBibTeX::checkBibTeX(QSharedPointer<Entry> &entry, const File *file, QWidget *parent) { /// disable GUI under process QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); /// use a dummy BibTeX file to collect all elements necessary for check File dummyFile; /// create temporary entry to work with dummyFile << entry; /// fetch and inser crossref'ed entry QString crossRefStr; Value crossRefVal = entry->value(Entry::ftCrossRef); if (!crossRefVal.isEmpty() && file != nullptr) { crossRefStr = PlainTextValue::text(crossRefVal); - QSharedPointer<Entry> crossRefDest = file->containsKey(crossRefStr, File::etEntry).dynamicCast<Entry>(); + QSharedPointer<Entry> crossRefDest = file->containsKey(crossRefStr, File::ElementType::Entry).dynamicCast<Entry>(); if (!crossRefDest.isNull()) dummyFile << crossRefDest; else crossRefStr.clear(); /// memorize crossref'ing failed } /// include all macro definitions, in case they are referenced if (file != nullptr) for (const auto &element : const_cast<const File &>(*file)) if (Macro::isMacro(*element)) dummyFile << element; /// run special exporter to get BibTeX's output QStringList bibtexOuput; QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); - FileExporterBibTeXOutput exporter(FileExporterBibTeXOutput::BibTeXLogFile, parent); + FileExporterBibTeXOutput exporter(FileExporterBibTeXOutput::OutputType::BibTeXLogFile, parent); bool exporterResult = exporter.save(&buffer, &dummyFile, &bibtexOuput); buffer.close(); if (!exporterResult) { QApplication::restoreOverrideCursor(); KMessageBox::errorList(parent, i18n("Running BibTeX failed.\n\nSee the following output to trace the error:"), bibtexOuput, i18n("Running BibTeX failed.")); - return FailedToCheck; + return CheckBibTeXResult::FailedToCheck; } /// define variables how to parse BibTeX's output static const QString warningStart = QStringLiteral("Warning--"); static const QRegularExpression warningEmptyField(QStringLiteral("empty (\\w+) in ")); static const QRegularExpression warningEmptyField2(QStringLiteral("empty (\\w+) or (\\w+) in ")); static const QRegularExpression warningThereIsBut(QStringLiteral("there's a (\\w+) but no (\\w+) in")); static const QRegularExpression warningCantUseBoth(QStringLiteral("can't use both (\\w+) and (\\w+) fields")); static const QRegularExpression warningSort2(QStringLiteral("to sort, need (\\w+) or (\\w+) in ")); static const QRegularExpression warningSort3(QStringLiteral("to sort, need (\\w+), (\\w+), or (\\w+) in ")); static const QRegularExpression errorLine(QStringLiteral("---line (\\d+)")); /// go line-by-line through BibTeX output and collect warnings/errors QStringList warnings; QString errorPlainText; for (const QString &line : const_cast<const QStringList &>(bibtexOuput)) { QRegularExpressionMatch match; if ((match = errorLine.match(line)).hasMatch()) { buffer.open(QIODevice::ReadOnly); QTextStream ts(&buffer); bool ok = false; for (int i = match.captured(1).toInt(&ok); ok && i > 1; --i) { errorPlainText = ts.readLine(); buffer.close(); } } else if (line.startsWith(QStringLiteral("Warning--"))) { /// is a warning ... if ((match = warningEmptyField.match(line)).hasMatch()) { /// empty/missing field warnings << i18n("Field <b>%1</b> is empty", match.captured(1)); } else if ((match = warningEmptyField2.match(line)).hasMatch()) { /// two empty/missing fields warnings << i18n("Fields <b>%1</b> and <b>%2</b> are empty, but at least one is required", match.captured(1), match.captured(2)); } else if ((match = warningThereIsBut.match(line)).hasMatch()) { /// there is a field which exists but another does not exist warnings << i18n("Field <b>%1</b> exists, but <b>%2</b> does not exist", match.captured(1), match.captured(2)); } else if ((match = warningCantUseBoth.match(line)).hasMatch()) { /// there are two conflicting fields, only one may be used warnings << i18n("Fields <b>%1</b> and <b>%2</b> cannot be used at the same time", match.captured(1), match.captured(2)); } else if ((match = warningSort2.match(line)).hasMatch()) { /// one out of two fields missing for sorting warnings << i18n("Fields <b>%1</b> or <b>%2</b> are required to sort entry", match.captured(1), match.captured(2)); } else if ((match = warningSort3.match(line)).hasMatch()) { /// one out of three fields missing for sorting warnings << i18n("Fields <b>%1</b>, <b>%2</b>, <b>%3</b> are required to sort entry", match.captured(1), match.captured(2), match.captured(3)); } else { /// generic/unknown warning warnings << i18n("Unknown warning: %1", line.mid(warningStart.length())); } } } - CheckBibTeXResult result = NoProblem; + CheckBibTeXResult result = CheckBibTeXResult::NoProblem; QApplication::restoreOverrideCursor(); if (!errorPlainText.isEmpty()) { - result = BibTeXWarning; + result = CheckBibTeXResult::BibTeXWarning; KMessageBox::information(parent, i18n("<qt><p>The following error was found:</p><pre>%1</pre></qt>", errorPlainText), i18n("Errors found")); } else if (!warnings.isEmpty()) { KMessageBox::informationList(parent, i18n("The following warnings were found:"), warnings, i18n("Warnings found")); - result = BibTeXError; + result = CheckBibTeXResult::BibTeXError; } else KMessageBox::information(parent, i18n("No warnings or errors were found.%1", crossRefStr.isEmpty() ? QString() : i18n("\n\nSome fields missing in this entry were taken from the crossref'ed entry '%1'.", crossRefStr)), i18n("No Errors or Warnings")); return result; } diff --git a/src/processing/checkbibtex.h b/src/processing/checkbibtex.h index 909b1d6d..8f17ffd6 100644 --- a/src/processing/checkbibtex.h +++ b/src/processing/checkbibtex.h @@ -1,43 +1,43 @@ /*************************************************************************** - * Copyright (C) 2004-2014 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_PROCESSING_CHECKBIBTEX_H #define KBIBTEX_PROCESSING_CHECKBIBTEX_H #include <QSharedPointer> #ifdef HAVE_KF5 #include "kbibtexprocessing_export.h" #endif // HAVE_KF5 class Entry; class Element; class File; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXPROCESSING_EXPORT CheckBibTeX { public: - enum CheckBibTeXResult { NoProblem, BibTeXWarning, BibTeXError, InvalidData, FailedToCheck }; + enum class CheckBibTeXResult { NoProblem, BibTeXWarning, BibTeXError, InvalidData, FailedToCheck }; static CheckBibTeXResult checkBibTeX(QSharedPointer<Element> &element, const File *file, QWidget *parent); static CheckBibTeXResult checkBibTeX(QSharedPointer<Entry> &entry, const File *file, QWidget *parent); }; #endif // KBIBTEX_PROCESSING_CHECKBIBTEX_H diff --git a/src/processing/findduplicates.cpp b/src/processing/findduplicates.cpp index 0b4f0e29..fc791d01 100644 --- a/src/processing/findduplicates.cpp +++ b/src/processing/findduplicates.cpp @@ -1,547 +1,547 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "findduplicates.h" #include <typeinfo> #include <QLinkedList> #include <QProgressDialog> #include <QApplication> #include <QDate> #include <QRegularExpression> #include <KLocalizedString> #include <File> #include <models/FileModel> #include <Entry> EntryClique::EntryClique() { /// nothing } int EntryClique::entryCount() const { return checkedEntries.count(); } QList<QSharedPointer<Entry> > EntryClique::entryList() const { return checkedEntries.keys(); } bool EntryClique::isEntryChecked(QSharedPointer<Entry> entry) const { return checkedEntries[entry]; } void EntryClique::setEntryChecked(QSharedPointer<Entry> entry, bool isChecked) { checkedEntries[entry] = isChecked; recalculateValueMap(); } int EntryClique::fieldCount() const { return valueMap.count(); } QList<QString> EntryClique::fieldList() const { return valueMap.keys(); } QVector<Value> EntryClique::values(const QString &field) const { return valueMap[field]; } QVector<Value> &EntryClique::values(const QString &field) { return valueMap[field]; } Value EntryClique::chosenValue(const QString &field) const { Q_ASSERT_X(chosenValueMap[field].count() == 1, "Value EntryClique::chosenValue(const QString &field) const", "Exactly one value expected in chosenValueMap"); return chosenValueMap[field].first(); } QVector<Value> EntryClique::chosenValues(const QString &field) const { return chosenValueMap[field]; } void EntryClique::setChosenValue(const QString &field, const Value &value, ValueOperation valueOperation) { switch (valueOperation) { - case SetValue: { + case ValueOperation::SetValue: { chosenValueMap[field].clear(); chosenValueMap[field] << value; break; } - case AddValue: { + case ValueOperation::AddValue: { QString text = PlainTextValue::text(value); for (const Value &value : const_cast<const QVector<Value> &>(chosenValueMap[field])) if (PlainTextValue::text(value) == text) return; chosenValueMap[field] << value; break; } - case RemoveValue: { + case ValueOperation::RemoveValue: { QString text = PlainTextValue::text(value); for (QVector<Value>::Iterator it = chosenValueMap[field].begin(); it != chosenValueMap[field].end(); ++it) if (PlainTextValue::text(*it) == text) { chosenValueMap[field].erase(it); return; } break; } } } void EntryClique::addEntry(QSharedPointer<Entry> entry) { checkedEntries.insert(entry, false); /// remember to call recalculateValueMap later } void EntryClique::recalculateValueMap() { valueMap.clear(); chosenValueMap.clear(); /// go through each and every entry ... const QList<QSharedPointer<Entry> > el = entryList(); for (const auto &entry : el) if (isEntryChecked(entry)) { /// cover entry type Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(entry->type()))); insertKeyValueToValueMap(QStringLiteral("^type"), v, entry->type(), Qt::CaseInsensitive /** entry types shall be compared case insensitive */); /// cover entry id v.clear(); v.append(QSharedPointer<VerbatimText>(new VerbatimText(entry->id()))); insertKeyValueToValueMap(QStringLiteral("^id"), v, entry->id()); /// go through each and every field of this entry for (Entry::ConstIterator fieldIt = entry->constBegin(); fieldIt != entry->constEnd(); ++fieldIt) { /// store both field name and value for later reference const QString fieldName = fieldIt.key().toLower(); const Value fieldValue = fieldIt.value(); if (fieldName == Entry::ftKeywords || fieldName == Entry::ftUrl) { for (const auto &vi : fieldValue) { const QString text = PlainTextValue::text(*vi); Value v; v << vi; insertKeyValueToValueMap(fieldName, v, text); } } else { const QString fieldValueText = PlainTextValue::text(fieldValue); insertKeyValueToValueMap(fieldName, fieldValue, fieldValueText); } } } const auto fl = fieldList(); for (const QString &fieldName : fl) if (valueMap[fieldName].count() < 2) { valueMap.remove(fieldName); chosenValueMap.remove(fieldName); } } void EntryClique::insertKeyValueToValueMap(const QString &fieldName, const Value &fieldValue, const QString &fieldValueText, const Qt::CaseSensitivity) { if (fieldValueText.isEmpty()) return; if (valueMap.contains(fieldName)) { /// in the list of alternatives, search of a value identical /// to the current (as of fieldIt) value (to avoid duplicates) bool alreadyContained = false; QVector<Value> alternatives = valueMap[fieldName]; for (const Value &v : const_cast<const QVector<Value> &>(alternatives)) if (PlainTextValue::text(v) == fieldValueText) { alreadyContained = true; break; } if (!alreadyContained) { alternatives << fieldValue; valueMap[fieldName] = alternatives; } } else { QVector<Value> alternatives = valueMap[fieldName]; alternatives << fieldValue; valueMap.insert(fieldName, alternatives); QVector<Value> chosen; chosen << fieldValue; chosenValueMap.insert(fieldName, chosen); } } class FindDuplicates::FindDuplicatesPrivate { private: const unsigned int maxDistance; int **d; static const int dsize; public: int sensitivity; QWidget *widget; FindDuplicatesPrivate(int sens, QWidget *w) : maxDistance(10000), sensitivity(sens), widget(w == nullptr ? qApp->activeWindow() : w) { d = new int *[dsize]; for (int i = 0; i < dsize; ++i) d[i] = new int[dsize]; } ~FindDuplicatesPrivate() { for (int i = 0; i < dsize; ++i) delete[] d[i]; delete [] d; } /** * Determine the Levenshtein distance between two words. * See also http://en.wikipedia.org/wiki/Levenshtein_distance * @param s first word, all chars already in lower case * @param t second word, all chars already in lower case * @return distance between both words */ double levenshteinDistanceWord(const QString &s, const QString &t) { const int m = qMin(s.length(), dsize - 1), n = qMin(t.length(), dsize - 1); if (m < 1 && n < 1) return 0.0; if (m < 1 || n < 1) return 1.0; for (int i = 0; i <= m; ++i) d[i][0] = i; for (int i = 0; i <= n; ++i) d[0][i] = i; for (int i = 1; i <= m; ++i) for (int j = 1; j <= n; ++j) { d[i][j] = d[i - 1][j] + 1; int c = d[i][j - 1] + 1; if (c < d[i][j]) d[i][j] = c; c = d[i - 1][j - 1] + (s[i - 1] == t[j - 1] ? 0 : 1); if (c < d[i][j]) d[i][j] = c; } double result = d[m][n]; result = result / qMax(m, n); result *= result; return result; } /** * Determine the Levenshtein distance between two sentences (list of words). * See also http://en.wikipedia.org/wiki/Levenshtein_distance * @param s first sentence * @param t second sentence * @return distance between both sentences */ double levenshteinDistance(const QStringList &s, const QStringList &t) { const int m = s.size(), n = t.size(); if (m < 1 && n < 1) return 0.0; if (m < 1 || n < 1) return 1.0; double **d = new double*[m + 1]; for (int i = 0; i <= m; ++i) { d[i] = new double[n + 1]; d[i][0] = i; } for (int i = 0; i <= n; ++i) d[0][i] = i; for (int i = 1; i <= m; ++i) for (int j = 1; j <= n; ++j) { d[i][j] = d[i - 1][j] + 1; double c = d[i][j - 1] + 1; if (c < d[i][j]) d[i][j] = c; c = d[i - 1][j - 1] + levenshteinDistanceWord(s[i - 1], t[j - 1]); if (c < d[i][j]) d[i][j] = c; } double result = d[m][n]; for (int i = 0; i <= m; ++i) delete[] d[i]; delete [] d; result = result / qMax(m, n); return result; } /** * Determine the Levenshtein distance between two sentences, * where each sentence is in a string (not split into single words). * See also http://en.wikipedia.org/wiki/Levenshtein_distance * @param s first sentence * @param t second sentence * @return distance between both sentences */ double levenshteinDistance(const QString &s, const QString &t) { static const QRegularExpression nonWordRegExp(QStringLiteral("[^a-z']+"), QRegularExpression::CaseInsensitiveOption); if (s.isEmpty() || t.isEmpty()) return 1.0; return levenshteinDistance(s.toLower().split(nonWordRegExp, QString::SkipEmptyParts), t.toLower().split(nonWordRegExp, QString::SkipEmptyParts)); } /** * Distance between two BibTeX entries, scaled by maxDistance. */ int entryDistance(Entry *entryA, Entry *entryB) { /// "distance" to be used if no value for a field is given const double neutralDistance = 0.05; /** * Get both entries' titles. If both are empty, use a "neutral * distance" otherwise compute levenshtein distance (0.0 .. 1.0). */ const QString titleA = PlainTextValue::text(entryA->value(Entry::ftTitle)); const QString titleB = PlainTextValue::text(entryB->value(Entry::ftTitle)); double titleDistance = titleA.isEmpty() && titleB.isEmpty() ? neutralDistance : levenshteinDistance(titleA, titleB); /** * Get both entries' author names. If both are empty, use a * "neutral distance" otherwise compute levenshtein distance * (0.0 .. 1.0). */ const QString authorA = PlainTextValue::text(entryA->value(Entry::ftAuthor)); const QString authorB = PlainTextValue::text(entryB->value(Entry::ftAuthor)); double authorDistance = authorA.isEmpty() && authorB.isEmpty() ? neutralDistance : levenshteinDistance(authorA, authorB); /** * Get both entries' years. If both are empty, use a * "neutral distance" otherwise compute distance as follows: * take square of difference between both years, but impose * a maximum of 100. Divide value by 100.0 to get a distance * value of 0.0 .. 1.0. */ const QString yearA = PlainTextValue::text(entryA->value(Entry::ftYear)); const QString yearB = PlainTextValue::text(entryB->value(Entry::ftYear)); bool yearAok = false, yearBok = false; int yearAint = yearA.toInt(&yearAok); int yearBint = yearB.toInt(&yearBok); double yearDistance = yearAok && yearBok ? qMin((yearBint - yearAint) * (yearBint - yearAint), 100) / 100.0 : neutralDistance; /** * Compute total distance by taking individual distances for * author, title, and year. Weight each individual distance as * follows: title => 60%, author => 30%, year => 10% * Scale distance by maximum distance and round to int; result * will be in range 0 .. maxDistance. */ int distance = static_cast<int>(maxDistance * (titleDistance * 0.6 + authorDistance * 0.3 + yearDistance * 0.1) + 0.5); return distance; } }; const int FindDuplicates::FindDuplicatesPrivate::dsize = 32; FindDuplicates::FindDuplicates(QWidget *parent, int sensitivity) : QObject(parent), d(new FindDuplicatesPrivate(sensitivity, parent)) { /// nothing } FindDuplicates::~FindDuplicates() { delete d; } bool FindDuplicates::findDuplicateEntries(File *file, QVector<EntryClique *> &entryCliqueList) { QApplication::setOverrideCursor(Qt::WaitCursor); QScopedPointer<QProgressDialog> progressDlg(new QProgressDialog(i18n("Searching ..."), i18n("Cancel"), 0, 100000 /* to be set later to actual value */, d->widget)); progressDlg->setModal(true); progressDlg->setWindowTitle(i18n("Finding Duplicates")); progressDlg->setMinimumWidth(d->widget->fontMetrics().averageCharWidth() * 48); progressDlg->setAutoReset(false); entryCliqueList.clear(); /// assemble list of entries only (ignoring comments, macros, ...) QVector<QSharedPointer<Entry> > listOfEntries; listOfEntries.reserve(file->size()); for (const auto &element : const_cast<const File &>(*file)) { QSharedPointer<Entry> e = element.dynamicCast<Entry>(); if (!e.isNull() && !e->isEmpty()) listOfEntries << e; } if (listOfEntries.isEmpty()) { /// no entries to compare found entryCliqueList.clear(); QApplication::restoreOverrideCursor(); return progressDlg->wasCanceled(); } int curProgress = 0, maxProgress = listOfEntries.count() * (listOfEntries.count() - 1) / 2; int progressDelta = 1; progressDlg->setMaximum(maxProgress); progressDlg->show(); emit maximumProgress(maxProgress); /// go through all entries ... for (const auto &entry : const_cast<const QVector<QSharedPointer<Entry> > &>(listOfEntries)) { QApplication::instance()->processEvents(); if (progressDlg->wasCanceled()) { entryCliqueList.clear(); break; } progressDlg->setValue(curProgress); emit currentProgress(curProgress); /// ... and find a "clique" of entries where it will match, i.e. distance is below sensitivity /// assume current entry will match in no clique bool foundClique = false; /// go through all existing cliques for (QVector<EntryClique *>::Iterator cit = entryCliqueList.begin(); cit != entryCliqueList.end(); ++cit) { /// check distance between current entry and clique's first entry if (d->entryDistance(entry.data(), (*cit)->entryList().constFirst().data()) < d->sensitivity) { /// if distance is below sensitivity, add current entry to clique foundClique = true; (*cit)->addEntry(entry); break; } QApplication::instance()->processEvents(); if (progressDlg->wasCanceled()) { entryCliqueList.clear(); break; } } if (!progressDlg->wasCanceled() && !foundClique) { /// no clique matched to current entry, so create and add new clique /// consisting only of the current entry EntryClique *newClique = new EntryClique(); newClique->addEntry(entry); entryCliqueList << newClique; } curProgress += progressDelta; ++progressDelta; progressDlg->setValue(curProgress); emit currentProgress(curProgress); } progressDlg->setValue(progressDlg->maximum()); /// remove cliques with only one element (nothing to merge here) from the list of cliques for (QVector<EntryClique *>::Iterator cit = entryCliqueList.begin(); cit != entryCliqueList.end();) if ((*cit)->entryCount() < 2) { EntryClique *ec = *cit; cit = entryCliqueList.erase(cit); delete ec; } else { /// entries have been inserted as checked, /// therefore recalculate alternatives (*cit)->recalculateValueMap(); ++cit; } QApplication::restoreOverrideCursor(); return progressDlg->wasCanceled(); } MergeDuplicates::MergeDuplicates() { /// nothing } bool MergeDuplicates::mergeDuplicateEntries(const QVector<EntryClique *> &entryCliques, FileModel *fileModel) { bool didMerge = false; for (EntryClique *entryClique : entryCliques) { /// Avoid adding fields 20 lines below /// which have been remove (not added) 10 lines below QSet<QString> coveredFields; Entry *mergedEntry = new Entry(QString(), QString()); const auto fieldList = entryClique->fieldList(); coveredFields.reserve(fieldList.size()); for (const auto &field : fieldList) { coveredFields << field; if (field == QStringLiteral("^id")) mergedEntry->setId(PlainTextValue::text(entryClique->chosenValue(field))); else if (field == QStringLiteral("^type")) mergedEntry->setType(PlainTextValue::text(entryClique->chosenValue(field))); else { Value combined; const auto chosenValues = entryClique->chosenValues(field); for (const Value &v : chosenValues) { combined.append(v); } if (!combined.isEmpty()) mergedEntry->insert(field, combined); } } bool actuallyMerged = false; int preferredInsertionRow = -1; const auto entryList = entryClique->entryList(); for (const auto &entry : entryList) { /// if merging entries with identical ids, the merged entry will not yet have an id (is null) if (mergedEntry->id().isEmpty()) mergedEntry->setId(entry->id()); /// if merging entries with identical types, the merged entry will not yet have an type (is null) if (mergedEntry->type().isEmpty()) mergedEntry->setType(entry->type()); /// add all other fields not covered by user selection /// those fields did only occur in one entry (no conflict) /// may add a lot of bloat to merged entry if (entryClique->isEntryChecked(entry)) { actuallyMerged = true; for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) if (!mergedEntry->contains(it.key()) && !coveredFields.contains(it.key())) { mergedEntry->insert(it.key(), it.value()); coveredFields << it.key(); } const int row = fileModel->row(entry); if (preferredInsertionRow < 0) preferredInsertionRow = row; fileModel->removeRow(row); } } if (actuallyMerged) { if (preferredInsertionRow < 0) preferredInsertionRow = fileModel->rowCount(); fileModel->insertRow(QSharedPointer<Entry>(mergedEntry), preferredInsertionRow); } else delete mergedEntry; didMerge |= actuallyMerged; } return didMerge; } diff --git a/src/processing/findduplicates.h b/src/processing/findduplicates.h index 90eb51d1..c1c29ca7 100644 --- a/src/processing/findduplicates.h +++ b/src/processing/findduplicates.h @@ -1,108 +1,108 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_PROCESSING_FINDDUPLICATES_H #define KBIBTEX_PROCESSING_FINDDUPLICATES_H #include <QObject> #include <QMap> #include <Value> #ifdef HAVE_KF5 #include "kbibtexprocessing_export.h" #endif // HAVE_KF5 class Entry; class File; class FileModel; class KBIBTEXPROCESSING_EXPORT FindDuplicates; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXPROCESSING_EXPORT EntryClique { friend class FindDuplicates; public: EntryClique(); - enum ValueOperation { SetValue, AddValue, RemoveValue }; + enum class ValueOperation { SetValue, AddValue, RemoveValue }; int entryCount() const; QList<QSharedPointer<Entry> > entryList() const; bool isEntryChecked(QSharedPointer<Entry> entry) const; void setEntryChecked(QSharedPointer<Entry> entry, bool isChecked); int fieldCount() const; QList<QString> fieldList() const; QVector<Value> values(const QString &field) const; QVector<Value> &values(const QString &field); Value chosenValue(const QString &field) const; QVector<Value> chosenValues(const QString &field) const; - void setChosenValue(const QString &field, const Value &value, ValueOperation valueOperation = SetValue); + void setChosenValue(const QString &field, const Value &value, ValueOperation valueOperation = ValueOperation::SetValue); QString dump() const; protected: void addEntry(QSharedPointer<Entry> entry); private: QMap<QSharedPointer<Entry>, bool> checkedEntries; QMap<QString, QVector<Value> > valueMap; QMap<QString, QVector<Value> > chosenValueMap; void recalculateValueMap(); void insertKeyValueToValueMap(const QString &fieldName, const Value &fieldValue, const QString &fieldValueText, const Qt::CaseSensitivity = Qt::CaseSensitive); }; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXPROCESSING_EXPORT FindDuplicates : public QObject { Q_OBJECT public: explicit FindDuplicates(QWidget *parent, int sensitivity = 4000); ~FindDuplicates() override; bool findDuplicateEntries(File *file, QVector<EntryClique *> &entryCliqueList); signals: void maximumProgress(int maxProgress); void currentProgress(int progress); private: class FindDuplicatesPrivate; FindDuplicatesPrivate *d; }; /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXPROCESSING_EXPORT MergeDuplicates { public: static bool mergeDuplicateEntries(const QVector<EntryClique *> &entryCliques, FileModel *fileModel); private: explicit MergeDuplicates(); }; #endif // KBIBTEX_PROCESSING_FINDDUPLICATES_H diff --git a/src/processing/idsuggestions.cpp b/src/processing/idsuggestions.cpp index e3f3fe75..63ab4640 100644 --- a/src/processing/idsuggestions.cpp +++ b/src/processing/idsuggestions.cpp @@ -1,567 +1,567 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "idsuggestions.h" #include <QRegularExpression> #include <KLocalizedString> #include <Preferences> #include <Encoder> #include "journalabbreviations.h" class IdSuggestions::IdSuggestionsPrivate { private: IdSuggestions *p; static const QStringList smallWords; public: IdSuggestionsPrivate(IdSuggestions *parent) : p(parent) { /// nothing } QString normalizeText(const QString &input) const { static const QRegularExpression unwantedChars(QStringLiteral("[^-_:/=+a-zA-Z0-9]+")); return Encoder::instance().convertToPlainAscii(input).remove(unwantedChars); } int numberFromEntry(const Entry &entry, const QString &field) const { static const QRegularExpression firstDigits(QStringLiteral("^[0-9]+")); const QString text = PlainTextValue::text(entry.value(field)); const QRegularExpressionMatch match = firstDigits.match(text); if (!match.hasMatch()) return -1; bool ok = false; const int result = match.captured(0).toInt(&ok); return ok ? result : -1; } QString pageNumberFromEntry(const Entry &entry) const { static const QRegularExpression whitespace(QStringLiteral("[ \t]+")); static const QRegularExpression pageNumber(QStringLiteral("[a-z0-9+:]+"), QRegularExpression::CaseInsensitiveOption); const QString text = PlainTextValue::text(entry.value(Entry::ftPages)).remove(whitespace).remove(QStringLiteral("mbox")); const QRegularExpressionMatch match = pageNumber.match(text); if (!match.hasMatch()) return QString(); return match.captured(0); } QString translateTitleToken(const Entry &entry, const struct IdSuggestionTokenInfo &tti, bool removeSmallWords) const { QString result; bool first = true; static const QRegularExpression sequenceOfSpaces(QStringLiteral("\\s+")); const QStringList titleWords = PlainTextValue::text(entry.value(Entry::ftTitle)).split(sequenceOfSpaces, QString::SkipEmptyParts); int index = 0; for (QStringList::ConstIterator it = titleWords.begin(); it != titleWords.end(); ++it, ++index) { const QString lowerText = normalizeText(*it).toLower(); if ((removeSmallWords && smallWords.contains(lowerText)) || index < tti.startWord || index > tti.endWord) continue; if (first) first = false; else result.append(tti.inBetween); QString titleComponent = lowerText.left(tti.len); - if (tti.caseChange == IdSuggestions::ccToCamelCase) + if (tti.caseChange == IdSuggestions::CaseChange::ToCamelCase) titleComponent = titleComponent[0].toUpper() + titleComponent.mid(1); result.append(titleComponent); } switch (tti.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: result = result.toUpper(); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: result = result.toLower(); break; - case IdSuggestions::ccToCamelCase: - /// already processed above - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::ToCamelCase: + /// already processed above + case IdSuggestions::CaseChange::None: /// nothing break; } return result; } QString translateAuthorsToken(const Entry &entry, const struct IdSuggestionTokenInfo &ati) const { QString result; /// Already some author inserted into result? bool firstInserted = false; /// Get list of authors' last names const QStringList authors = entry.authorsLastName(); /// Keep track of which author (number/position) is processed int index = 0; /// Go through all authors for (QStringList::ConstIterator it = authors.constBegin(); it != authors.constEnd(); ++it, ++index) { /// Get current author, normalize name (remove unwanted characters), cut to maximum length QString author = normalizeText(*it).left(ati.len); /// Check if camel case is requests - if (ati.caseChange == IdSuggestions::ccToCamelCase) { + if (ati.caseChange == IdSuggestions::CaseChange::ToCamelCase) { /// Get components of the author's last name const QStringList nameComponents = author.split(QStringLiteral(" "), QString::SkipEmptyParts); QStringList newNameComponents; newNameComponents.reserve(nameComponents.size()); /// Camel-case each name component for (const QString &nameComponent : nameComponents) { newNameComponents.append(nameComponent[0].toUpper() + nameComponent.mid(1)); } /// Re-assemble name from camel-cased components author = newNameComponents.join(QStringLiteral(" ")); } if ( (index >= ati.startWord && index <= ati.endWord) ///< check for requested author range || (ati.lastWord && index == authors.count() - 1) ///< explicitly insert last author if requested in lastWord flag ) { if (firstInserted) result.append(ati.inBetween); result.append(author); firstInserted = true; } } switch (ati.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: result = result.toUpper(); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: result = result.toLower(); break; - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: /// already processed above break; - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::None: /// nothing break; } return result; } QString translateJournalToken(const Entry &entry, const struct IdSuggestionTokenInfo &jti, bool removeSmallWords) const { static const QRegularExpression sequenceOfSpaces(QStringLiteral("\\s+")); QString journalName = PlainTextValue::text(entry.value(Entry::ftJournal)); journalName = JournalAbbreviations::instance().toShortName(journalName); const QStringList journalWords = journalName.split(sequenceOfSpaces, QString::SkipEmptyParts); bool first = true; int index = 0; QString result; for (QStringList::ConstIterator it = journalWords.begin(); it != journalWords.end(); ++it, ++index) { QString journalComponent = normalizeText(*it); const QString lowerText = journalComponent.toLower(); if ((removeSmallWords && smallWords.contains(lowerText)) || index < jti.startWord || index > jti.endWord) continue; if (first) first = false; else result.append(jti.inBetween); /// Try to keep sequences of capital letters at the start of the journal name, /// those may already be abbreviations. int countCaptialCharsAtStart = 0; while (journalComponent[countCaptialCharsAtStart].isUpper()) ++countCaptialCharsAtStart; journalComponent = journalComponent.left(qMax(jti.len, countCaptialCharsAtStart)); - if (jti.caseChange == IdSuggestions::ccToCamelCase) + if (jti.caseChange == IdSuggestions::CaseChange::ToCamelCase) journalComponent = journalComponent[0].toUpper() + journalComponent.mid(1); result.append(journalComponent); } switch (jti.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: result = result.toUpper(); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: result = result.toLower(); break; - case IdSuggestions::ccToCamelCase: - /// already processed above - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::ToCamelCase: + /// already processed above + case IdSuggestions::CaseChange::None: /// nothing break; } return result; } QString translateTypeToken(const Entry &entry, const struct IdSuggestionTokenInfo &eti) const { QString entryType(entry.type()); switch (eti.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: return entryType.toUpper().left(eti.len); - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: return entryType.toLower().left(eti.len); - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: { if (entryType.isEmpty()) return QString(); ///< empty entry type? Return immediately to avoid problems with entryType[0] /// Apply some heuristic replacements to make the entry type look like CamelCase entryType = entryType.toLower(); ///< start with lower case /// Then, replace known words with their CamelCase variant entryType = entryType.replace(QStringLiteral("report"), QStringLiteral("Report")).replace(QStringLiteral("proceedings"), QStringLiteral("Proceedings")).replace(QStringLiteral("thesis"), QStringLiteral("Thesis")).replace(QStringLiteral("book"), QStringLiteral("Book")).replace(QStringLiteral("phd"), QStringLiteral("PhD")); /// Finally, guarantee that first letter is upper case entryType[0] = entryType[0].toUpper(); return entryType.left(eti.len); } default: return entryType.left(eti.len); } } QString translateToken(const Entry &entry, const QString &token) const { switch (token[0].toLatin1()) { case 'a': ///< deprecated but still supported case { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati struct IdSuggestionTokenInfo ati = p->evalToken(token.mid(1)); ati.startWord = ati.endWord = 0; ///< only first author return translateAuthorsToken(entry, ati); } case 'A': { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati const struct IdSuggestionTokenInfo ati = p->evalToken(token.mid(1)); return translateAuthorsToken(entry, ati); } case 'z': ///< deprecated but still supported case { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo ati struct IdSuggestionTokenInfo ati = p->evalToken(token.mid(1)); /// All but first author ati.startWord = 1; ati.endWord = std::numeric_limits<int>::max(); return translateAuthorsToken(entry, ati); } case 'y': { int year = numberFromEntry(entry, Entry::ftYear); if (year > -1) return QString::number(year % 100 + 100).mid(1); break; } case 'Y': { const int year = numberFromEntry(entry, Entry::ftYear); if (year > -1) return QString::number(year % 10000 + 10000).mid(1); break; } case 't': case 'T': { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo jti const struct IdSuggestionTokenInfo tti = p->evalToken(token.mid(1)); return translateTitleToken(entry, tti, token[0].isUpper()); } case 'j': case 'J': { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo jti const struct IdSuggestionTokenInfo jti = p->evalToken(token.mid(1)); return translateJournalToken(entry, jti, token[0].isUpper()); } case 'e': { /// Evaluate the token string, store information in struct IdSuggestionTokenInfo eti const struct IdSuggestionTokenInfo eti = p->evalToken(token.mid(1)); return translateTypeToken(entry, eti); } case 'v': { return normalizeText(PlainTextValue::text(entry.value(Entry::ftVolume))); } case 'p': { return pageNumberFromEntry(entry); } case '"': return token.mid(1); } return QString(); } }; /// List of small words taken from OCLC: /// https://www.oclc.org/developer/develop/web-services/worldcat-search-api/bibliographic-resource.en.html const QStringList IdSuggestions::IdSuggestionsPrivate::smallWords = i18nc("Small words that can be removed from titles when generating id suggestions; separated by pipe symbol", "a|als|am|an|are|as|at|auf|aus|be|but|by|das|dass|de|der|des|dich|dir|du|er|es|for|from|had|have|he|her|his|how|ihr|ihre|ihres|im|in|is|ist|it|kein|la|le|les|mein|mich|mir|mit|of|on|sein|sie|that|the|this|to|un|une|von|was|wer|which|wie|wird|with|yousie|that|the|this|to|un|une|von|was|wer|which|wie|wird|with|you").split(QStringLiteral("|"), QString::SkipEmptyParts); IdSuggestions::IdSuggestions() : d(new IdSuggestionsPrivate(this)) { /// nothing } IdSuggestions::~IdSuggestions() { delete d; } QString IdSuggestions::formatId(const Entry &entry, const QString &formatStr) const { QString id; const QStringList tokenList = formatStr.split(QStringLiteral("|"), QString::SkipEmptyParts); for (const QString &token : tokenList) { id.append(d->translateToken(entry, token)); } return id; } QString IdSuggestions::defaultFormatId(const Entry &entry) const { return formatId(entry, Preferences::instance().activeIdSuggestionFormatString()); } bool IdSuggestions::hasDefaultFormat() const { return !Preferences::instance().activeIdSuggestionFormatString().isEmpty(); } bool IdSuggestions::applyDefaultFormatId(Entry &entry) const { const QString dfs = Preferences::instance().activeIdSuggestionFormatString(); if (!dfs.isEmpty()) { entry.setId(defaultFormatId(entry)); return true; } else return false; } QStringList IdSuggestions::formatIdList(const Entry &entry) const { const QStringList formatStrings = Preferences::instance().idSuggestionFormatStrings(); QStringList result; result.reserve(formatStrings.size()); for (const QString &formatString : formatStrings) { result << formatId(entry, formatString); } return result; } QStringList IdSuggestions::formatStrToHuman(const QString &formatStr) const { QStringList result; const QStringList tokenList = formatStr.split(QStringLiteral("|"), QString::SkipEmptyParts); for (const QString &token : tokenList) { QString text; if (token[0] == 'a' || token[0] == 'A' || token[0] == 'z') { struct IdSuggestionTokenInfo info = evalToken(token.mid(1)); if (token[0] == 'a') info.startWord = info.endWord = 0; else if (token[0] == 'z') { info.startWord = 1; info.endWord = std::numeric_limits<int>::max(); } text = formatAuthorRange(info.startWord, info.endWord, info.lastWord); if (info.len > 0 && info.len < std::numeric_limits<int>::max()) text.append(i18np(", but only first letter of each last name", ", but only first %1 letters of each last name", info.len)); switch (info.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: text.append(i18n(", in upper case")); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: text.append(i18n(", in lower case")); break; - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: text.append(i18n(", in CamelCase")); break; - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::None: break; } if (!info.inBetween.isEmpty()) text.append(i18n(", with '%1' in between", info.inBetween)); } else if (token[0] == 'y') text.append(i18n("Year (2 digits)")); else if (token[0] == 'Y') text.append(i18n("Year (4 digits)")); else if (token[0] == 't' || token[0] == 'T') { struct IdSuggestionTokenInfo info = evalToken(token.mid(1)); text.append(i18n("Title")); if (info.startWord == 0 && info.endWord < std::numeric_limits<int>::max()) text.append(i18np(", but only the first word", ", but only first %1 words", info.endWord + 1)); else if (info.startWord > 0 && info.endWord == std::numeric_limits<int>::max()) text.append(i18n(", but only starting from word %1", info.startWord + 1)); else if (info.startWord > 0 && info.endWord < std::numeric_limits<int>::max()) text.append(i18n(", but only from word %1 to word %2", info.startWord + 1, info.endWord + 1)); if (info.len > 0 && info.len < std::numeric_limits<int>::max()) text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len)); switch (info.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: text.append(i18n(", in upper case")); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: text.append(i18n(", in lower case")); break; - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: text.append(i18n(", in CamelCase")); break; - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::None: break; } if (!info.inBetween.isEmpty()) text.append(i18n(", with '%1' in between", info.inBetween)); if (token[0] == 'T') text.append(i18n(", small words removed")); } else if (token[0] == 'j') { struct IdSuggestionTokenInfo info = evalToken(token.mid(1)); text.append(i18n("Journal")); if (info.len > 0 && info.len < std::numeric_limits<int>::max()) text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len)); switch (info.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: text.append(i18n(", in upper case")); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: text.append(i18n(", in lower case")); break; - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: text.append(i18n(", in CamelCase")); break; - case IdSuggestions::ccNoChange: + case IdSuggestions::CaseChange::None: break; } } else if (token[0] == 'e') { struct IdSuggestionTokenInfo info = evalToken(token.mid(1)); text.append(i18n("Type")); if (info.len > 0 && info.len < std::numeric_limits<int>::max()) text.append(i18np(", but only first letter of each word", ", but only first %1 letters of each word", info.len)); switch (info.caseChange) { - case IdSuggestions::ccToUpper: + case IdSuggestions::CaseChange::ToUpper: text.append(i18n(", in upper case")); break; - case IdSuggestions::ccToLower: + case IdSuggestions::CaseChange::ToLower: text.append(i18n(", in lower case")); break; - case IdSuggestions::ccToCamelCase: + case IdSuggestions::CaseChange::ToCamelCase: text.append(i18n(", in CamelCase")); break; default: break; } } else if (token[0] == 'v') { text.append(i18n("Volume")); } else if (token[0] == 'p') { text.append(i18n("First page number")); } else if (token[0] == '"') text.append(i18n("Text: '%1'", token.mid(1))); else text.append("?"); result.append(text); } return result; } QString IdSuggestions::formatAuthorRange(int minValue, int maxValue, bool lastAuthor) { if (minValue == 0) { if (maxValue == 0) { if (lastAuthor) return i18n("First and last authors only"); else return i18n("First author only"); } else if (maxValue == std::numeric_limits<int>::max()) return i18n("All authors"); else { if (lastAuthor) return i18n("From first author to author %1 and last author", maxValue + 1); else return i18n("From first author to author %1", maxValue + 1); } } else if (minValue == 1) { if (maxValue == std::numeric_limits<int>::max()) return i18n("All but first author"); else { if (lastAuthor) return i18n("From author %1 to author %2 and last author", minValue + 1, maxValue + 1); else return i18n("From author %1 to author %2", minValue + 1, maxValue + 1); } } else { if (maxValue == std::numeric_limits<int>::max()) return i18n("From author %1 to last author", minValue + 1); else if (lastAuthor) return i18n("From author %1 to author %2 and last author", minValue + 1, maxValue + 1); else return i18n("From author %1 to author %2", minValue + 1, maxValue + 1); } } struct IdSuggestions::IdSuggestionTokenInfo IdSuggestions::evalToken(const QString &token) const { int pos = 0; struct IdSuggestionTokenInfo result; result.len = std::numeric_limits<int>::max(); result.startWord = 0; result.endWord = std::numeric_limits<int>::max(); result.lastWord = false; - result.caseChange = IdSuggestions::ccNoChange; + result.caseChange = IdSuggestions::CaseChange::None; result.inBetween = QString(); if (token.length() > pos) { int dv = token[pos].digitValue(); if (dv > -1) { result.len = dv; ++pos; } } if (token.length() > pos) { switch (token[pos].unicode()) { case 0x006c: // 'l' - result.caseChange = IdSuggestions::ccToLower; + result.caseChange = IdSuggestions::CaseChange::ToLower; ++pos; break; case 0x0075: // 'u' - result.caseChange = IdSuggestions::ccToUpper; + result.caseChange = IdSuggestions::CaseChange::ToUpper; ++pos; break; case 0x0063: // 'c' - result.caseChange = IdSuggestions::ccToCamelCase; + result.caseChange = IdSuggestions::CaseChange::ToCamelCase; ++pos; break; default: - result.caseChange = IdSuggestions::ccNoChange; + result.caseChange = IdSuggestions::CaseChange::None; } } int dvStart = 0, dvEnd = std::numeric_limits<int>::max(); if (token.length() > pos + 2 ///< sufficiently many characters to follow && token[pos] == 'w' ///< identifier to start specifying a range of words && (dvStart = token[pos + 1].digitValue()) > -1 ///< first word index correctly parsed && ( token[pos + 2] == QLatin1Char('I') ///< infinitely many words || (dvEnd = token[pos + 2].digitValue()) > -1) ///< last word index finite and correctly parsed ) { result.startWord = dvStart; result.endWord = dvEnd; pos += 3; /// Optionally, the last word (e.g. last author) is explicitly requested if (token.length() > pos && token[pos] == QLatin1Char('L')) { result.lastWord = true; ++pos; } } if (token.length() > pos + 1 && token[pos] == '"') result.inBetween = token.mid(pos + 1); return result; } diff --git a/src/processing/idsuggestions.h b/src/processing/idsuggestions.h index e3712d7a..bf030f4a 100644 --- a/src/processing/idsuggestions.h +++ b/src/processing/idsuggestions.h @@ -1,78 +1,78 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_PROCESSING_IDSUGGESTIONS_H #define KBIBTEX_PROCESSING_IDSUGGESTIONS_H #include <Entry> #ifdef HAVE_KF5 #include "kbibtexprocessing_export.h" #endif // HAVE_KF5 /** * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXPROCESSING_EXPORT IdSuggestions { public: - enum CaseChange {ccNoChange = 0, ccToUpper = 1, ccToLower = 2, ccToCamelCase = 3}; + enum class CaseChange {None, ToUpper, ToLower, ToCamelCase}; struct IdSuggestionTokenInfo { int len; int startWord, endWord; bool lastWord; CaseChange caseChange; QString inBetween; }; IdSuggestions(); IdSuggestions(const IdSuggestions &) = delete; IdSuggestions &operator= (const IdSuggestions &other) = delete; ~IdSuggestions(); QString formatId(const Entry &entry, const QString &formatStr) const; QString defaultFormatId(const Entry &entry) const; bool hasDefaultFormat() const; /** * Apply the default formatting string to the entry. * If no default formatting string is set, the entry * will stay untouched and the function return false. * If the formatting string is set, the entry's id * will be changed accordingly and the function returns true. * * @param entry entry where the id has to be set * @return true if the id was set, false otherwise */ bool applyDefaultFormatId(Entry &entry) const; QStringList formatIdList(const Entry &entry) const; QStringList formatStrToHuman(const QString &formatStr) const; static QString formatAuthorRange(int minValue, int maxValue, bool lastAuthor); protected: struct IdSuggestionTokenInfo evalToken(const QString &token) const; private: class IdSuggestionsPrivate; IdSuggestionsPrivate *d; }; #endif // KBIBTEX_PROCESSING_IDSUGGESTIONS_H diff --git a/src/program/docklets/documentpreview.cpp b/src/program/docklets/documentpreview.cpp index 131e712a..7095f059 100644 --- a/src/program/docklets/documentpreview.cpp +++ b/src/program/docklets/documentpreview.cpp @@ -1,691 +1,691 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "documentpreview.h" #include <typeinfo> #include <QDomDocument> #include <QDomElement> #include <QList> #include <QLayout> #include <QMap> #include <QFileInfo> #include <QResizeEvent> #include <QCheckBox> #include <QMenuBar> #include <QStackedWidget> #include <QDockWidget> #include <QPushButton> #include <QComboBox> #include <QMutex> #include <QMimeType> #include <QIcon> #ifdef HAVE_WEBENGINEWIDGETS #include <QWebEngineView> #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS #include <QWebView> #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS #include <KLocalizedString> #include <KJobWidgets> #include <KRun> #include <KMimeTypeTrader> #include <KService> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <kio/jobclasses.h> #include <kio/job.h> #include <kio/jobuidelegate.h> #include <KToolBar> #include <KActionCollection> #include <KSharedConfig> #include <KConfigGroup> #include <kio_version.h> #include <KBibTeX> #include <Element> #include <Entry> #include <File> #include <FileInfo> #include "logging_program.h" ImageLabel::ImageLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) : QLabel(text, parent, f) { /// nothing } void ImageLabel::setPixmap(const QPixmap &pixmap) { m_pixmap = pixmap; if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= width() && m_pixmap.height() <= height() ? m_pixmap : pixmap.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } else QLabel::setPixmap(m_pixmap); } void ImageLabel::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= event->size().width() && m_pixmap.height() <= event->size().height() ? m_pixmap : m_pixmap.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } } class DocumentPreview::DocumentPreviewPrivate { public: struct UrlInfo { QUrl url; QString mimeType; QIcon icon; }; private: DocumentPreview *p; KSharedConfigPtr config; static const QString configGroupName; static const QString onlyLocalFilesCheckConfig; QPushButton *externalViewerButton; QStackedWidget *stackedWidget; ImageLabel *message; QMap<int, struct UrlInfo> cbxEntryToUrlInfo; QMutex addingUrlMutex; static const QString arXivPDFUrlStart; bool anyLocal; QMenuBar *menuBar; KToolBar *toolBar; KParts::ReadOnlyPart *okularPart; #ifdef HAVE_WEBENGINEWIDGETS QWebEngineView *htmlWidget; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS QWebView *htmlWidget; #else // HAVE_WEBKITWIDGETS KParts::ReadOnlyPart *htmlPart; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS int swpMessage, swpOkular, swpHTML; public: QComboBox *urlComboBox; QPushButton *onlyLocalFilesButton; QList<KIO::StatJob *> runningJobs; QSharedPointer<const Entry> entry; QUrl baseUrl; bool anyRemote; KParts::ReadOnlyPart *locatePart(const QString &mimeType, QWidget *parentWidget) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(mimeType, QStringLiteral("KParts/ReadOnlyPart")); if (service) { KParts::ReadOnlyPart *part = service->createInstance<KParts::ReadOnlyPart>(parentWidget, p); connect(part, static_cast<void(KParts::ReadOnlyPart::*)()>(&KParts::ReadOnlyPart::completed), p, &DocumentPreview::loadingFinished); return part; } else return nullptr; } DocumentPreviewPrivate(DocumentPreview *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), anyLocal(false), entry(nullptr), anyRemote(false) { setupGUI(); } /** * Create user interface for this widget. * It consists of some controlling widget on the top, * but the most space is consumed by KPart widgets * inside a QStackedWidget to show the external content * (PDF file, web page, ...). */ void setupGUI() { QVBoxLayout *layout = new QVBoxLayout(p); layout->setMargin(0); /// some widgets on the top to control the view QHBoxLayout *innerLayout = new QHBoxLayout(); layout->addLayout(innerLayout, 0); onlyLocalFilesButton = new QPushButton(QIcon::fromTheme(QStringLiteral("applications-internet")), QString(), p); onlyLocalFilesButton->setToolTip(i18n("Toggle between local files only and all documents including remote ones")); innerLayout->addWidget(onlyLocalFilesButton, 0); onlyLocalFilesButton->setCheckable(true); QSizePolicy sp = onlyLocalFilesButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); onlyLocalFilesButton->setSizePolicy(sp); urlComboBox = new QComboBox(p); innerLayout->addWidget(urlComboBox, 1); externalViewerButton = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), QString(), p); externalViewerButton->setToolTip(i18n("Open in external program")); innerLayout->addWidget(externalViewerButton, 0); sp = externalViewerButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); externalViewerButton->setSizePolicy(sp); menuBar = new QMenuBar(p); menuBar->setBackgroundRole(QPalette::Window); menuBar->setVisible(false); layout->addWidget(menuBar, 0); toolBar = new KToolBar(p); toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); toolBar->setBackgroundRole(QPalette::Window); toolBar->setVisible(false); layout->addWidget(toolBar, 0); /// main part of the widget stackedWidget = new QStackedWidget(p); layout->addWidget(stackedWidget, 1); /// default widget if no preview is available message = new ImageLabel(i18n("No preview available"), stackedWidget); message->setAlignment(Qt::AlignCenter); message->setWordWrap(true); swpMessage = stackedWidget->addWidget(message); connect(message, &QLabel::linkActivated, p, &DocumentPreview::linkActivated); /// add parts to stackedWidget okularPart = locatePart(QStringLiteral("application/pdf"), stackedWidget); swpOkular = (okularPart == nullptr) ? -1 : stackedWidget->addWidget(okularPart->widget()); if (okularPart == nullptr || swpOkular < 0) { qCWarning(LOG_KBIBTEX_PROGRAM) << "No 'KDE Framworks 5'-based Okular part for PDF or PostScript document preview available."; } #ifdef HAVE_WEBENGINEWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebEngine is available, using it instead of WebKit or HTML KPart (both neither considered nor tested for) for HTML/Web preview."; /// To make DrKonqi handle crashes in Chromium-based QtWebEngine, /// set a certain environment variable. For details, see here: /// https://www.dvratil.cz/2018/10/drkonqi-and-qtwebengine/ /// https://phabricator.kde.org/D16004 const auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"); if (!chromiumFlags.contains("disable-in-process-stack-traces")) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags + " --disable-in-process-stack-traces"); } htmlWidget = new QWebEngineView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebEngineView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebKit is available, using it instead of WebEngine (missing) or HTML KPart (not considered) for HTML/Web preview."; htmlWidget = new QWebView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBKITWIDGETS htmlPart = locatePart(QStringLiteral("text/html"), stackedWidget); if (htmlPart != nullptr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "HTML KPart is available, using it instead of WebEngine or WebKit (neither available) for HTML/Web preview."; swpHTML = stackedWidget->addWidget(htmlPart->widget()); } else { qCDebug(LOG_KBIBTEX_PROGRAM) << "No HTML viewing component is available, disabling HTML/Web preview."; swpHTML = -1; } #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS loadState(); connect(externalViewerButton, &QPushButton::clicked, p, [this]() { openExternally(); }); connect(urlComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, [this](int index) { comboBoxChanged(index); }); connect(onlyLocalFilesButton, &QPushButton::toggled, p, &DocumentPreview::onlyLocalFilesChanged); } bool addUrl(const struct UrlInfo &urlInfo) { bool isLocal = KBibTeX::isLocalOrRelative(urlInfo.url); anyLocal |= isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) return true; ///< ignore URL if only local files are allowed if (isLocal) { /// create a drop-down list entry if file is a local file /// (based on patch by Luis Silva) QString fn = urlInfo.url.fileName(); QString full = urlInfo.url.url(QUrl::PreferLocalFile); QString dir = urlInfo.url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString text = fn.isEmpty() ? full : (dir.isEmpty() ? fn : QString(QStringLiteral("%1 [%2]")).arg(fn, dir)); urlComboBox->addItem(urlInfo.icon, text); } else { /// create a drop-down list entry if file is a remote file urlComboBox->addItem(urlInfo.icon, urlInfo.url.toDisplayString()); } urlComboBox->setEnabled(true); cbxEntryToUrlInfo.insert(urlComboBox->count() - 1, urlInfo); externalViewerButton->setEnabled(true); if (urlComboBox->count() == 1 || ///< first entry in combobox isLocal || ///< local files always preferred over URLs /// prefer arXiv summary URLs over other URLs (!anyLocal && urlInfo.url.host().contains(QStringLiteral("arxiv.org/abs")))) { showUrl(urlInfo); } return true; } void update() { p->setCursor(Qt::WaitCursor); /// reset and clear all controls if (swpOkular >= 0 && okularPart != nullptr) okularPart->closeUrl(); #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS urlComboBox->setEnabled(false); urlComboBox->clear(); cbxEntryToUrlInfo.clear(); externalViewerButton->setEnabled(false); showMessage(i18n("Refreshing...")); // krazy:exclude=qmethods /// cancel/kill all running jobs auto it = runningJobs.begin(); while (it != runningJobs.end()) { (*it)->kill(); it = runningJobs.erase(it); } /// clear flag that memorizes if any local file was referenced anyLocal = false; anyRemote = false; /// do not load external reference if widget is hidden if (isVisible()) { - const auto urlList = FileInfo::entryUrls(entry, baseUrl, FileInfo::TestExistenceYes); + const auto urlList = FileInfo::entryUrls(entry, baseUrl, FileInfo::TestExistence::Yes); for (const QUrl &url : urlList) { bool isLocal = KBibTeX::isLocalOrRelative(url); anyRemote |= !isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) continue; KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, 3, KIO::HideProgressInfo); runningJobs << job; KJobWidgets::setWindow(job, p); connect(job, &KIO::StatJob::result, p, &DocumentPreview::statFinished); } if (urlList.isEmpty()) { /// Case no URLs associated with this entry. /// For-loop above was never executed. showMessage(i18n("No documents to show.")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } else if (runningJobs.isEmpty()) { /// Case no stat jobs are running. As there were URLs (tested in /// previous condition), this implies that there were remote /// references that were ignored by executing "continue" above. /// Give user hint that by enabling remote files, more can be shown. showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } } else p->setCursor(Qt::ArrowCursor); } void showMessage(const QString &msgText) { stackedWidget->setCurrentIndex(swpMessage); message->setPixmap(QPixmap()); message->setText(msgText); if (swpOkular >= 0) stackedWidget->widget(swpOkular)->setEnabled(false); if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); menuBar->setVisible(false); toolBar->setVisible(true); menuBar->clear(); toolBar->clear(); } void setupToolMenuBarForPart(const KParts::ReadOnlyPart *part) { /* KAction *printAction = KStandardAction::print(part, SLOT(slotPrint()), part->actionCollection()); printAction->setEnabled(false); connect(part, SIGNAL(enablePrintAction(bool)), printAction, SLOT(setEnabled(bool))); */ QDomDocument doc = part->domDocument(); QDomElement docElem = doc.documentElement(); QDomNodeList toolbarNodes = docElem.elementsByTagName(QStringLiteral("ToolBar")); for (int i = 0; i < toolbarNodes.count(); ++i) { QDomNodeList toolbarItems = toolbarNodes.at(i).childNodes(); for (int j = 0; j < toolbarItems.count(); ++j) { QDomNode toolbarItem = toolbarItems.at(j); if (toolbarItem.nodeName() == QStringLiteral("Action")) { QString actionName = toolbarItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); toolBar->addAction(part->actionCollection()->action(actionName)); } else if (toolbarItem.nodeName() == QStringLiteral("Separator")) { toolBar->addSeparator(); } } } QDomNodeList menubarNodes = docElem.elementsByTagName(QStringLiteral("MenuBar")); for (int i = 0; i < menubarNodes.count(); ++i) { QDomNodeList menubarNode = menubarNodes.at(i).childNodes(); for (int j = 0; j < menubarNode.count(); ++j) { QDomNode menubarItem = menubarNode.at(j); if (menubarItem.nodeName() == QStringLiteral("Menu")) { QDomNodeList menuNode = menubarItem.childNodes(); QString text; for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("text")) { text = menuItem.firstChild().toText().data(); break; } } QMenu *menu = menuBar->addMenu(text); for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("Action")) { QString actionName = menuItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); menu->addAction(part->actionCollection()->action(actionName)); } else if (menuItem.nodeName() == QStringLiteral("Separator")) { menu->addSeparator(); } } } } } QDomNodeList actionPropertiesList = docElem.elementsByTagName(QStringLiteral("ActionProperties")); for (int i = 0; i < actionPropertiesList.count(); ++i) { QDomNodeList actionProperties = actionPropertiesList.at(i).childNodes(); for (int j = 0; j < actionProperties.count(); ++j) { QDomNode actionNode = actionProperties.at(j); if (actionNode.nodeName() == QStringLiteral("Action")) { const QString actionName = actionNode.attributes().namedItem(QStringLiteral("name")).toAttr().nodeValue(); const QString actionShortcut = actionNode.attributes().namedItem(QStringLiteral("shortcut")).toAttr().value(); QAction *action = part->actionCollection()->action(actionName); if (action != nullptr) { action->setShortcut(QKeySequence(actionShortcut)); } } } } menuBar->setVisible(true); toolBar->setVisible(true); } void showPart(const KParts::ReadOnlyPart *part, QWidget *widget) { menuBar->setVisible(false); toolBar->setVisible(false); menuBar->clear(); toolBar->clear(); if (okularPart != nullptr && part == okularPart && swpOkular >= 0) { stackedWidget->setCurrentIndex(swpOkular); stackedWidget->widget(swpOkular)->setEnabled(true); setupToolMenuBarForPart(okularPart); #ifdef HAVE_WEBENGINEWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBKITWIDGETS } else if (htmlPart != nullptr && part == htmlPart && swpHTML >= 0) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); setupToolMenuBarForPart(htmlPart); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (widget == message) { stackedWidget->setCurrentIndex(swpMessage); } else showMessage(i18n("Cannot show requested part")); // krazy:exclude=qmethods } bool showUrl(const struct UrlInfo &urlInfo) { static const QStringList okularMimetypes {QStringLiteral("application/x-pdf"), QStringLiteral("application/pdf"), QStringLiteral("application/x-gzpdf"), QStringLiteral("application/x-bzpdf"), QStringLiteral("application/x-wwf"), QStringLiteral("image/vnd.djvu"), QStringLiteral("image/vnd.djvu+multipage"), QStringLiteral("application/postscript"), QStringLiteral("image/x-eps"), QStringLiteral("application/x-gzpostscript"), QStringLiteral("application/x-bzpostscript"), QStringLiteral("image/x-gzeps"), QStringLiteral("image/x-bzeps")}; static const QStringList htmlMimetypes {QStringLiteral("text/html"), QStringLiteral("application/xml"), QStringLiteral("application/xhtml+xml")}; static const QStringList imageMimetypes {QStringLiteral("image/jpeg"), QStringLiteral("image/png"), QStringLiteral("image/gif"), QStringLiteral("image/tiff")}; if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); if (swpOkular >= 0 && okularPart != nullptr) { stackedWidget->widget(swpOkular)->setEnabled(false); okularPart->closeUrl(); } #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS if (swpOkular >= 0 && okularPart != nullptr && okularMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods return okularPart->openUrl(urlInfo.url); } else if (htmlMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBKITWIDGETS return (swpHTML >= 0 && htmlPart != nullptr) ? htmlPart->openUrl(urlInfo.url) : false; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (imageMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); message->setPixmap(QPixmap(urlInfo.url.url(QUrl::PreferLocalFile))); showPart(nullptr, message); p->unsetCursor(); return true; } else { QString additionalInformation; if (urlInfo.mimeType == QStringLiteral("application/pdf")) additionalInformation = i18nc("Additional information in case there is not KPart available for mime type 'application/pdf'", "<br/><br/>Please install <a href=\"https://userbase.kde.org/Okular\">Okular</a> for KDE Frameworks&nbsp;5 to make use of its PDF viewing component.<br/>Okular for KDE&nbsp;4 will not work."); showMessage(i18nc("First parameter is mime type, second parameter is optional information (may be empty)", "<qt>Don't know how to show mimetype '%1'.%2</qt>", urlInfo.mimeType, additionalInformation)); // krazy:exclude=qmethods } return false; } void openExternally() { QUrl url(cbxEntryToUrlInfo[urlComboBox->currentIndex()].url); /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags()); } UrlInfo urlMetaInfo(const QUrl &url) { UrlInfo result; result.url = url; if (!KBibTeX::isLocalOrRelative(url) && url.fileName().isEmpty()) { /// URLs not pointing to a specific file should be opened with a web browser component result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); return result; } const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); result.mimeType = mimeType.name(); result.icon = QIcon::fromTheme(mimeType.iconName()); if (result.mimeType == QStringLiteral("application/octet-stream")) { /// application/octet-stream is a fall-back if KDE did not know better result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } else if ((result.mimeType.isEmpty() || result.mimeType == QStringLiteral("inode/directory")) && (result.url.scheme() == QStringLiteral("http") || result.url.scheme() == QStringLiteral("https"))) { /// directory via http means normal webpage (not browsable directory) result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } if (url.url(QUrl::PreferLocalFile).startsWith(arXivPDFUrlStart)) { result.icon = QIcon::fromTheme(QStringLiteral("application-pdf")); result.mimeType = QStringLiteral("application/pdf"); } return result; } void comboBoxChanged(int index) { showUrl(cbxEntryToUrlInfo[index]); } bool isVisible() { /// get dock where this widget is inside /// static cast is save as constructor requires parent to be QDockWidget QDockWidget *pp = static_cast<QDockWidget *>(p->parent()); return pp != nullptr && !pp->isHidden(); } void loadState() { KConfigGroup configGroup(config, configGroupName); onlyLocalFilesButton->setChecked(!configGroup.readEntry(onlyLocalFilesCheckConfig, true)); } void saveState() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(onlyLocalFilesCheckConfig, !onlyLocalFilesButton->isChecked()); config->sync(); } }; const QString DocumentPreview::DocumentPreviewPrivate::arXivPDFUrlStart = QStringLiteral("http://arxiv.org/pdf/"); const QString DocumentPreview::DocumentPreviewPrivate::configGroupName = QStringLiteral("URL Preview"); const QString DocumentPreview::DocumentPreviewPrivate::onlyLocalFilesCheckConfig = QStringLiteral("OnlyLocalFiles"); DocumentPreview::DocumentPreview(QDockWidget *parent) : QWidget(parent), d(new DocumentPreviewPrivate(this)) { connect(parent, &QDockWidget::visibilityChanged, this, [this]() { d->update(); }); } DocumentPreview::~DocumentPreview() { delete d; } void DocumentPreview::setElement(QSharedPointer<Element> element, const File *) { d->entry = element.dynamicCast<const Entry>(); d->update(); } void DocumentPreview::setBibTeXUrl(const QUrl &url) { d->baseUrl = url; } void DocumentPreview::onlyLocalFilesChanged() { d->saveState(); d->update(); } void DocumentPreview::statFinished(KJob *kjob) { KIO::StatJob *job = static_cast<KIO::StatJob *>(kjob); d->runningJobs.removeOne(job); if (!job->error()) { const QUrl url = job->mostLocalUrl(); DocumentPreviewPrivate::UrlInfo urlInfo = d->urlMetaInfo(url); setCursor(d->runningJobs.isEmpty() ? Qt::ArrowCursor : Qt::BusyCursor); d->addUrl(urlInfo); } else { qCWarning(LOG_KBIBTEX_PROGRAM) << job->error() << job->errorString(); } if (d->runningJobs.isEmpty()) { /// If this was the last background stat job ... setCursor(Qt::ArrowCursor); if (d->urlComboBox->count() < 1) { /// In case that no valid references were found by the stat jobs ... if (d->anyRemote && !d->onlyLocalFilesButton->isChecked()) { /// There are some remote URLs to probe, /// but user was only looking for local files d->showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods } else { /// No stat job at all succeeded. Show message to user. d->showMessage(i18n("No documents to show.\nSome URLs or files could not be retrieved.")); // krazy:exclude=qmethods } } } } void DocumentPreview::loadingFinished() { setCursor(Qt::ArrowCursor); d->showPart(qobject_cast<KParts::ReadOnlyPart *>(sender()), qobject_cast<QWidget *>(sender())); } void DocumentPreview::linkActivated(const QString &link) { if (link == QStringLiteral("disableonlylocalfiles")) d->onlyLocalFilesButton->setChecked(true); else if (link.startsWith(QStringLiteral("http://")) || link.startsWith(QStringLiteral("https://"))) { const QUrl urlToOpen = QUrl::fromUserInput(link); if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, this, KRun::RunFlags()); } } } diff --git a/src/program/docklets/filesettings.cpp b/src/program/docklets/filesettings.cpp index 9faabf9b..8f3afd27 100644 --- a/src/program/docklets/filesettings.cpp +++ b/src/program/docklets/filesettings.cpp @@ -1,77 +1,77 @@ /***************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * *****************************************************************************/ #include "filesettings.h" #include <QFormLayout> #include <QCheckBox> #include <QComboBox> #include <KLocalizedString> #include <Preferences> #include <Value> #include <File> #include <GUIHelper> #include <ItalicTextItemModel> #include <file/FileView> #include <models/FileModel> FileSettings::FileSettings(QWidget *parent) : FileSettingsWidget(parent), m_fileView(nullptr) { setEnabled(false); connect(this, &FileSettings::widgetsChanged, this, &FileSettings::widgetsChangedSlot); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::currentChanged, this, &FileSettings::currentFileChangedSlot); /// Monitoring file flag changes to get notified of /// "Save As" operations where the file settings /// may get changed (requires a reload of properties) connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &FileSettings::flagsChangedSlot); } void FileSettings::setFileView(FileView *fileView) { m_fileView = fileView; currentFileChangedSlot(); } void FileSettings::widgetsChangedSlot() { File *file = m_fileView != nullptr && m_fileView->fileModel() != nullptr ? m_fileView->fileModel()->bibliographyFile() : nullptr; if (file != nullptr) { saveProperties(file); /// Notify main view about change it its data m_fileView->externalModification(); } } void FileSettings::currentFileChangedSlot() { File *file = m_fileView != nullptr && m_fileView->fileModel() != nullptr ? m_fileView->fileModel()->bibliographyFile() : nullptr; loadProperties(file); setEnabled(file != nullptr); } void FileSettings::flagsChangedSlot(const OpenFileInfo::StatusFlags statusFlags) { - if (statusFlags.testFlag(OpenFileInfo::Open)) { + if (statusFlags.testFlag(OpenFileInfo::StatusFlag::Open)) { File *file = m_fileView != nullptr && m_fileView->fileModel() != nullptr ? m_fileView->fileModel()->bibliographyFile() : nullptr; loadProperties(file); } } diff --git a/src/program/docklets/referencepreview.cpp b/src/program/docklets/referencepreview.cpp index 1a9d3ccd..2a0f6529 100644 --- a/src/program/docklets/referencepreview.cpp +++ b/src/program/docklets/referencepreview.cpp @@ -1,414 +1,414 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * and contributors * * * * Contributions to this file were made by * * - Jurgen Spitzmuller <juergen@spitzmueller.org> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "referencepreview.h" #include <QFrame> #include <QBuffer> #include <QTextDocument> #include <QLayout> #include <QApplication> #include <QTextStream> #include <QTemporaryFile> #include <QPalette> #include <QMimeType> #include <QFileDialog> #include <QPushButton> #include <QFontDatabase> #include <QComboBox> #include <KLocalizedString> #include <KRun> #include <KIO/CopyJob> #include <KJobWidgets> #include <KSharedConfig> #include <KConfigGroup> #include <KTextEdit> #include <kio_version.h> #include <Element> #include <Entry> #include <File> #include <XSLTransform> #include <FileExporterBibTeX> #include <FileExporterBibTeX2HTML> #include <FileExporterRIS> #include <FileExporterXSLT> #include <file/FileView> #include "logging_program.h" static const struct PreviewStyles { QString label, style, type; } previewStyles[] = { {i18n("Source (BibTeX)"), QStringLiteral("bibtex"), QStringLiteral("exporter")}, {i18n("Source (RIS)"), QStringLiteral("ris"), QStringLiteral("exporter")}, {QStringLiteral("abbrv"), QStringLiteral("abbrv"), QStringLiteral("bibtex2html")}, {QStringLiteral("acm"), QStringLiteral("acm"), QStringLiteral("bibtex2html")}, {QStringLiteral("alpha"), QStringLiteral("alpha"), QStringLiteral("bibtex2html")}, {QStringLiteral("apalike"), QStringLiteral("apalike"), QStringLiteral("bibtex2html")}, {QStringLiteral("ieeetr"), QStringLiteral("ieeetr"), QStringLiteral("bibtex2html")}, {QStringLiteral("plain"), QStringLiteral("plain"), QStringLiteral("bibtex2html")}, {QStringLiteral("siam"), QStringLiteral("siam"), QStringLiteral("bibtex2html")}, {QStringLiteral("unsrt"), QStringLiteral("unsrt"), QStringLiteral("bibtex2html")}, {i18n("Standard"), QStringLiteral("standard"), QStringLiteral("xml")}, {i18n("Fancy"), QStringLiteral("fancy"), QStringLiteral("xml")}, {i18n("Wikipedia Citation"), QStringLiteral("wikipedia-cite"), QStringLiteral("plain_xml")}, {i18n("Abstract-only"), QStringLiteral("abstractonly"), QStringLiteral("xml")} }; Q_DECLARE_METATYPE(PreviewStyles) class ReferencePreview::ReferencePreviewPrivate { private: ReferencePreview *p; public: KSharedConfigPtr config; const QString configGroupName; const QString configKeyName; QPushButton *buttonOpen, *buttonSaveAsHTML; QString htmlText; QUrl baseUrl; QTextDocument *htmlDocument; KTextEdit *htmlView; QComboBox *comboBox; QSharedPointer<const Element> element; const File *file; FileView *fileView; const QColor textColor; const int defaultFontSize; const QString htmlStart; const QString notAvailableMessage; ReferencePreviewPrivate(ReferencePreview *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Reference Preview Docklet")), configKeyName(QStringLiteral("Style")), file(nullptr), fileView(nullptr), textColor(QApplication::palette().text().color()), defaultFontSize(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize()), htmlStart(QStringLiteral("<html>\n<head>\n<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n<style type=\"text/css\">\npre {\n white-space: pre-wrap;\n white-space: -moz-pre-wrap;\n white-space: -pre-wrap;\n white-space: -o-pre-wrap;\n word-wrap: break-word;\n}\n</style>\n</head>\n<body style=\"color: ") + textColor.name() + QStringLiteral("; font-size: ") + QString::number(defaultFontSize) + QStringLiteral("pt; font-family: '") + QFontDatabase::systemFont(QFontDatabase::GeneralFont).family() + QStringLiteral("'; background-color: '") + QApplication::palette().base().color().name(QColor::HexRgb) + QStringLiteral("'\">")), notAvailableMessage(htmlStart + QStringLiteral("<p style=\"font-style: italic;\">") + i18n("No preview available") + QStringLiteral("</p><p style=\"font-size: 90%;\">") + i18n("Reason:") + QStringLiteral(" %1</p></body></html>")) { QGridLayout *gridLayout = new QGridLayout(p); gridLayout->setMargin(0); gridLayout->setColumnStretch(0, 1); gridLayout->setColumnStretch(1, 0); gridLayout->setColumnStretch(2, 0); comboBox = new QComboBox(p); gridLayout->addWidget(comboBox, 0, 0, 1, 3); QFrame *frame = new QFrame(p); gridLayout->addWidget(frame, 1, 0, 1, 3); frame->setFrameShadow(QFrame::Sunken); frame->setFrameShape(QFrame::StyledPanel); QVBoxLayout *layout = new QVBoxLayout(frame); layout->setMargin(0); htmlView = new KTextEdit(frame); htmlView->setReadOnly(true); htmlDocument = new QTextDocument(htmlView); htmlView->setDocument(htmlDocument); layout->addWidget(htmlView); buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), p); buttonOpen->setToolTip(i18n("Open reference in web browser.")); gridLayout->addWidget(buttonOpen, 2, 1, 1, 1); buttonSaveAsHTML = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as HTML"), p); buttonSaveAsHTML->setToolTip(i18n("Save reference as HTML fragment.")); gridLayout->addWidget(buttonSaveAsHTML, 2, 2, 1, 1); } bool saveHTML(const QUrl &url) const { QTemporaryFile tempFile; tempFile.setAutoRemove(true); bool result = saveHTML(tempFile); if (result) { KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(tempFile.fileName()), url, KIO::Overwrite); KJobWidgets::setWindow(copyJob, p); result = copyJob->exec(); } return result; } bool saveHTML(QTemporaryFile &tempFile) const { if (tempFile.open()) { QTextStream ts(&tempFile); ts.setCodec("utf-8"); static const QRegularExpression kbibtexHrefRegExp(QStringLiteral("<a[^>]+href=\"kbibtex:[^>]+>(.+?)</a>")); QString modifiedHtmlText = htmlText; modifiedHtmlText = modifiedHtmlText.replace(kbibtexHrefRegExp, QStringLiteral("\\1")); ts << modifiedHtmlText; tempFile.close(); return true; } return false; } void loadState() { static bool hasBibTeX2HTML = !QStandardPaths::findExecutable(QStringLiteral("bibtex2html")).isEmpty(); KConfigGroup configGroup(config, configGroupName); const QString previousStyle = configGroup.readEntry(configKeyName, QString()); comboBox->clear(); int styleIndex = 0, c = 0; for (const PreviewStyles &previewStyle : previewStyles) { if (!hasBibTeX2HTML && previewStyle.type.contains(QStringLiteral("bibtex2html"))) continue; comboBox->addItem(previewStyle.label, QVariant::fromValue(previewStyle)); if (previousStyle == previewStyle.style) styleIndex = c; ++c; } comboBox->setCurrentIndex(styleIndex); } void saveState() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(configKeyName, comboBox->itemData(comboBox->currentIndex()).value<PreviewStyles>().style); config->sync(); } }; ReferencePreview::ReferencePreview(QWidget *parent) : QWidget(parent), d(new ReferencePreviewPrivate(this)) { d->loadState(); connect(d->buttonOpen, &QPushButton::clicked, this, &ReferencePreview::openAsHTML); connect(d->buttonSaveAsHTML, &QPushButton::clicked, this, &ReferencePreview::saveAsHTML); connect(d->comboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ReferencePreview::renderHTML); setEnabled(false); } ReferencePreview::~ReferencePreview() { delete d; } void ReferencePreview::setHtml(const QString &html, bool buttonsEnabled) { d->htmlText = QString(html).remove(QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")); d->htmlDocument->setHtml(d->htmlText); d->buttonOpen->setEnabled(buttonsEnabled); d->buttonSaveAsHTML->setEnabled(buttonsEnabled); } void ReferencePreview::setEnabled(bool enabled) { if (enabled) setHtml(d->htmlText, true); else setHtml(d->notAvailableMessage.arg(i18n("Preview disabled")), false); d->htmlView->setEnabled(enabled); d->comboBox->setEnabled(enabled); } void ReferencePreview::setElement(QSharedPointer<Element> element, const File *file) { d->element = element; d->file = file; renderHTML(); } void ReferencePreview::renderHTML() { enum { ignore, /// do not include crossref'ed entry's values (one entry) /// NOT USED: add, /// feed both the current entry as well as the crossref'ed entry into the exporter (two entries) merge /// merge the crossref'ed entry's values into the current entry (one entry) } crossRefHandling = ignore; if (d->element.isNull()) { setHtml(d->notAvailableMessage.arg(i18n("No element selected")), false); return; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); FileExporter *exporter = nullptr; const PreviewStyles previewStyle = d->comboBox->itemData(d->comboBox->currentIndex()).value<PreviewStyles>(); if (previewStyle.type == QStringLiteral("exporter")) { if (previewStyle.style == QStringLiteral("bibtex")) { FileExporterBibTeX *exporterBibTeX = new FileExporterBibTeX(this); exporterBibTeX->setEncoding(QStringLiteral("utf-8")); exporter = exporterBibTeX; } else if (previewStyle.style == QStringLiteral("ris")) exporter = new FileExporterRIS(this); else qCWarning(LOG_KBIBTEX_PROGRAM) << "Don't know how to handle output style " << previewStyle.style << " for type " << previewStyle.type; } else if (previewStyle.type == QStringLiteral("bibtex2html")) { crossRefHandling = merge; FileExporterBibTeX2HTML *exporterHTML = new FileExporterBibTeX2HTML(this); exporterHTML->setLaTeXBibliographyStyle(previewStyle.style); exporter = exporterHTML; } else if (previewStyle.type == QStringLiteral("xml") || previewStyle.type.endsWith(QStringLiteral("_xml"))) { crossRefHandling = merge; const QString filename = previewStyle.style + QStringLiteral(".xsl"); exporter = new FileExporterXSLT(XSLTransform::locateXSLTfile(filename), this); } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Don't know how to handle output type " << previewStyle.type; if (exporter != nullptr) { QBuffer buffer(this); buffer.open(QBuffer::WriteOnly); bool exporterResult = false; QStringList errorLog; QSharedPointer<const Entry> entry = d->element.dynamicCast<const Entry>(); /** NOT USED if (crossRefHandling == add && !entry.isNull()) { QString crossRef = PlainTextValue::text(entry->value(QStringLiteral("crossref"))); QSharedPointer<const Entry> crossRefEntry = d->file == NULL ? QSharedPointer<const Entry>() : d->file->containsKey(crossRef) .dynamicCast<const Entry>(); if (!crossRefEntry.isNull()) { File file; file.append(QSharedPointer<Entry>(new Entry(*entry))); file.append(QSharedPointer<Entry>(new Entry(*crossRefEntry))); exporterResult = exporter->save(&buffer, &file, &errorLog); } else exporterResult = exporter->save(&buffer, d->element, d->file, &errorLog); } else */ if (crossRefHandling == merge && !entry.isNull()) { QSharedPointer<Entry> merged = QSharedPointer<Entry>(entry->resolveCrossref(d->file)); exporterResult = exporter->save(&buffer, merged, d->file, &errorLog); } else exporterResult = exporter->save(&buffer, d->element, d->file, &errorLog); buffer.close(); delete exporter; buffer.open(QBuffer::ReadOnly); QString text = QString::fromUtf8(buffer.readAll().constData()); buffer.close(); bool buttonsEnabled = true; if (!exporterResult || text.isEmpty()) { /// something went wrong, no output ... text = d->notAvailableMessage.arg(i18n("No output generated")); buttonsEnabled = false; qCDebug(LOG_KBIBTEX_PROGRAM) << errorLog.join(QStringLiteral("\n")); } else { /// beautify text text.replace(QStringLiteral("``"), QStringLiteral("&ldquo;")); text.replace(QStringLiteral("''"), QStringLiteral("&rdquo;")); static const QRegularExpression openingSingleQuotationRegExp(QStringLiteral("(^|[> ,.;:!?])`(\\S)")); static const QRegularExpression closingSingleQuotationRegExp(QStringLiteral("(\\S)'([ ,.;:!?<]|$)")); text.replace(openingSingleQuotationRegExp, QStringLiteral("\\1&lsquo;\\2")); text.replace(closingSingleQuotationRegExp, QStringLiteral("\\1&rsquo;\\2")); if (previewStyle.style == QStringLiteral("wikipedia-cite")) text.remove(QStringLiteral("\n")); if (text.contains(QStringLiteral("{{cite FIXME"))) { /// Wikipedia {{cite ...}} command had problems (e.g. unknown entry type) text = d->notAvailableMessage.arg(i18n("This type of element is not supported by Wikipedia's <tt>{{cite}}</tt> command.")); } else if (previewStyle.type == QStringLiteral("exporter") || previewStyle.type.startsWith(QStringLiteral("plain_"))) { /// source text.prepend(QStringLiteral("';\">")); text.prepend(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()); text.prepend(QStringLiteral("<pre style=\"font-family: '")); text.prepend(d->htmlStart); text.append(QStringLiteral("</pre></body></html>")); } else if (previewStyle.type == QStringLiteral("bibtex2html")) { /// bibtex2html /// remove "generated by" line from HTML code if BibTeX2HTML was used text.remove(QRegularExpression(QStringLiteral("<hr><p><em>.*</p>"))); text.remove(QRegularExpression(QStringLiteral("<[/]?(font)[^>]*>"))); text.remove(QRegularExpression(QStringLiteral("^.*?<td.*?</td.*?<td>"))); text.remove(QRegularExpression(QStringLiteral("</td>.*$"))); text.remove(QRegularExpression(QStringLiteral("\\[ <a.*?</a> \\]"))); /// replace ASCII art with Unicode characters text.replace(QStringLiteral("---"), QString(QChar(0x2014))); text.replace(QStringLiteral("--"), QString(QChar(0x2013))); text.prepend(d->htmlStart); text.append("</body></html>"); } else if (previewStyle.type == QStringLiteral("xml")) { /// XML/XSLT text.prepend(d->htmlStart); text.append("</body></html>"); } /// adopt current color scheme text.replace(QStringLiteral("color: black;"), QString(QStringLiteral("color: %1;")).arg(d->textColor.name())); } setHtml(text, buttonsEnabled); d->saveState(); } else { /// something went wrong, no exporter ... setHtml(d->notAvailableMessage.arg(i18n("No output generated")), false); } QApplication::restoreOverrideCursor(); } void ReferencePreview::openAsHTML() { QTemporaryFile file(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("referencePreview-openAsHTML-XXXXXX.html")); file.setAutoRemove(false); /// let file stay alive for browser d->saveHTML(file); /// Ask KDE subsystem to open url in viewer matching mime type QUrl url(file.fileName()); KRun::runUrl(url, QStringLiteral("text/html"), this, KRun::RunFlags()); } void ReferencePreview::saveAsHTML() { QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save as HTML"), QUrl(), QStringLiteral("text/html")); if (url.isValid()) d->saveHTML(url); } void ReferencePreview::linkClicked(const QUrl &url) { QString text = url.toDisplayString(); if (text.startsWith(QStringLiteral("kbibtex:filter:"))) { text = text.mid(15); if (d->fileView != nullptr) { int p = text.indexOf(QStringLiteral("=")); SortFilterFileModel::FilterQuery fq; fq.terms << text.mid(p + 1); - fq.combination = SortFilterFileModel::EveryTerm; + fq.combination = SortFilterFileModel::FilterCombination::EveryTerm; fq.field = text.left(p); fq.searchPDFfiles = false; d->fileView->setFilterBarFilter(fq); } } } void ReferencePreview::setFileView(FileView *fileView) { d->fileView = fileView; } diff --git a/src/program/docklets/valuelist.cpp b/src/program/docklets/valuelist.cpp index 6c583e64..423466cf 100644 --- a/src/program/docklets/valuelist.cpp +++ b/src/program/docklets/valuelist.cpp @@ -1,488 +1,488 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "valuelist.h" #include <typeinfo> #include <QTreeView> #include <QHeaderView> #include <QGridLayout> #include <QStringListModel> #include <QScrollBar> #include <QLineEdit> #include <QComboBox> #include <QTimer> #include <QSortFilterProxyModel> #include <QAction> #include <KConfigGroup> #include <KLocalizedString> #include <KToggleAction> #include <KSharedConfig> #include <BibTeXFields> #include <Entry> #include <file/FileView> #include <ValueListModel> #include <models/FileModel> class ValueList::ValueListPrivate { private: ValueList *p; ValueListDelegate *delegate; public: KSharedConfigPtr config; const QString configGroupName; const QString configKeyFieldName, configKeyShowCountColumn, configKeySortByCountAction, configKeyHeaderState; FileView *fileView; QTreeView *treeviewFieldValues; ValueListModel *model; QSortFilterProxyModel *sortingModel; QComboBox *comboboxFieldNames; QLineEdit *lineeditFilter; const int countWidth; QAction *assignSelectionAction; QAction *removeSelectionAction; KToggleAction *showCountColumnAction; KToggleAction *sortByCountAction; ValueListPrivate(ValueList *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Value List Docklet")), configKeyFieldName(QStringLiteral("FieldName")), configKeyShowCountColumn(QStringLiteral("ShowCountColumn")), configKeySortByCountAction(QStringLiteral("SortByCountAction")), configKeyHeaderState(QStringLiteral("HeaderState")), fileView(nullptr), model(nullptr), sortingModel(nullptr), #if QT_VERSION >= 0x050b00 countWidth(8 + parent->fontMetrics().horizontalAdvance(i18n("Count"))) #else // QT_VERSION >= 0x050b00 countWidth(8 + parent->fontMetrics().width(i18n("Count"))) #endif // QT_VERSION >= 0x050b00 { setupGUI(); initialize(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); layout->setMargin(0); comboboxFieldNames = new QComboBox(p); comboboxFieldNames->setEditable(true); layout->addWidget(comboboxFieldNames); lineeditFilter = new QLineEdit(p); layout->addWidget(lineeditFilter); lineeditFilter->setClearButtonEnabled(true); lineeditFilter->setPlaceholderText(i18n("Filter value list")); treeviewFieldValues = new QTreeView(p); layout->addWidget(treeviewFieldValues); treeviewFieldValues->setEditTriggers(QAbstractItemView::EditKeyPressed); treeviewFieldValues->setSortingEnabled(true); treeviewFieldValues->sortByColumn(0, Qt::AscendingOrder); delegate = new ValueListDelegate(treeviewFieldValues); treeviewFieldValues->setItemDelegate(delegate); treeviewFieldValues->setRootIsDecorated(false); treeviewFieldValues->setSelectionMode(QTreeView::ExtendedSelection); treeviewFieldValues->setAlternatingRowColors(true); treeviewFieldValues->header()->setSectionResizeMode(QHeaderView::Fixed); treeviewFieldValues->setContextMenuPolicy(Qt::ActionsContextMenu); /// create context menu item to start renaming QAction *action = new QAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Replace all occurrences"), p); connect(action, &QAction::triggered, p, &ValueList::startItemRenaming); treeviewFieldValues->addAction(action); /// create context menu item to delete value action = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete all occurrences"), p); connect(action, &QAction::triggered, p, &ValueList::deleteAllOccurrences); treeviewFieldValues->addAction(action); /// create context menu item to search for multiple selections action = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search for selected values"), p); connect(action, &QAction::triggered, p, &ValueList::searchSelection); treeviewFieldValues->addAction(action); /// create context menu item to assign value to selected bibliography elements assignSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("emblem-new")), i18n("Add value to selected entries"), p); connect(assignSelectionAction, &QAction::triggered, p, &ValueList::assignSelection); treeviewFieldValues->addAction(assignSelectionAction); /// create context menu item to remove value from selected bibliography elements removeSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove value from selected entries"), p); connect(removeSelectionAction, &QAction::triggered, p, &ValueList::removeSelection); treeviewFieldValues->addAction(removeSelectionAction); p->setEnabled(false); connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &ValueList::fieldNamesChanged); connect(comboboxFieldNames, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), lineeditFilter, &QLineEdit::clear); connect(treeviewFieldValues, &QTreeView::activated, p, &ValueList::listItemActivated); connect(delegate, &ValueListDelegate::closeEditor, treeviewFieldValues, &QTreeView::reset); /// add context menu to header treeviewFieldValues->header()->setContextMenuPolicy(Qt::ActionsContextMenu); showCountColumnAction = new KToggleAction(i18n("Show Count Column"), treeviewFieldValues); connect(showCountColumnAction, &QAction::triggered, p, &ValueList::showCountColumnToggled); treeviewFieldValues->header()->addAction(showCountColumnAction); sortByCountAction = new KToggleAction(i18n("Sort by Count"), treeviewFieldValues); connect(sortByCountAction, &QAction::triggered, p, &ValueList::sortByCountToggled); treeviewFieldValues->header()->addAction(sortByCountAction); } void setComboboxFieldNamesCurrentItem(const QString &text) { int index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchExactly); if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchStartsWith); if (index < 0) index = comboboxFieldNames->findData(text, Qt::UserRole, Qt::MatchContains); if (index >= 0) comboboxFieldNames->setCurrentIndex(index); } void initialize() { lineeditFilter->clear(); comboboxFieldNames->clear(); for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { if (!fd.upperCamelCaseAlt.isEmpty()) continue; /// keep only "single" fields and not combined ones like "Author or Editor" if (fd.upperCamelCase.startsWith('^')) continue; /// skip "type" and "id" comboboxFieldNames->addItem(fd.label, fd.upperCamelCase); } /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(comboboxFieldNames); proxy->setSortLocaleAware(true); proxy->setSourceModel(comboboxFieldNames->model()); comboboxFieldNames->model()->setParent(proxy); comboboxFieldNames->setModel(proxy); comboboxFieldNames->model()->sort(0); KConfigGroup configGroup(config, configGroupName); QString fieldName = configGroup.readEntry(configKeyFieldName, QString(Entry::ftAuthor)); setComboboxFieldNamesCurrentItem(fieldName); if (allowsMultipleValues(fieldName)) assignSelectionAction->setText(i18n("Add value to selected entries")); else assignSelectionAction->setText(i18n("Replace value of selected entries")); showCountColumnAction->setChecked(configGroup.readEntry(configKeyShowCountColumn, true)); sortByCountAction->setChecked(configGroup.readEntry(configKeySortByCountAction, false)); sortByCountAction->setEnabled(!showCountColumnAction->isChecked()); QByteArray headerState = configGroup.readEntry(configKeyHeaderState, QByteArray()); treeviewFieldValues->header()->restoreState(headerState); connect(treeviewFieldValues->header(), &QHeaderView::sortIndicatorChanged, p, &ValueList::columnsChanged); } void update() { QString text = comboboxFieldNames->itemData(comboboxFieldNames->currentIndex()).toString(); if (text.isEmpty()) text = comboboxFieldNames->currentText(); delegate->setFieldName(text); model = fileView == nullptr ? nullptr : fileView->valueListModel(text); QAbstractItemModel *usedModel = model; if (usedModel != nullptr) { model->setShowCountColumn(showCountColumnAction->isChecked()); - model->setSortBy(sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText); + model->setSortBy(sortByCountAction->isChecked() ? ValueListModel::SortBy::Count : ValueListModel::SortBy::Text); if (sortingModel != nullptr) delete sortingModel; sortingModel = new QSortFilterProxyModel(p); sortingModel->setSourceModel(model); if (treeviewFieldValues->header()->isSortIndicatorShown()) sortingModel->sort(treeviewFieldValues->header()->sortIndicatorSection(), treeviewFieldValues->header()->sortIndicatorOrder()); else sortingModel->sort(1, Qt::DescendingOrder); sortingModel->setSortRole(ValueListModel::SortRole); sortingModel->setFilterKeyColumn(0); sortingModel->setFilterCaseSensitivity(Qt::CaseInsensitive); sortingModel->setFilterRole(ValueListModel::SearchTextRole); connect(lineeditFilter, &QLineEdit::textEdited, sortingModel, &QSortFilterProxyModel::setFilterFixedString); sortingModel->setSortLocaleAware(true); usedModel = sortingModel; } treeviewFieldValues->setModel(usedModel); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(configKeyFieldName, text); config->sync(); } bool allowsMultipleValues(const QString &field) const { return (field.compare(Entry::ftAuthor, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftEditor, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftUrl, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftFile, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftLocalFile, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftDOI, Qt::CaseInsensitive) == 0 || field.compare(Entry::ftKeywords, Qt::CaseInsensitive) == 0); } }; ValueList::ValueList(QWidget *parent) : QWidget(parent), d(new ValueListPrivate(this)) { QTimer::singleShot(500, this, [this]() { resizeEvent(nullptr); }); } ValueList::~ValueList() { delete d; } void ValueList::setFileView(FileView *fileView) { if (d->fileView != nullptr) disconnect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged); d->fileView = fileView; if (d->fileView != nullptr) { connect(d->fileView, &FileView::selectedElementsChanged, this, &ValueList::editorSelectionChanged); connect(d->fileView, &FileView::destroyed, this, &ValueList::editorDestroyed); } editorSelectionChanged(); update(); resizeEvent(nullptr); } void ValueList::update() { d->update(); setEnabled(d->fileView != nullptr); } void ValueList::resizeEvent(QResizeEvent *) { int widgetWidth = d->treeviewFieldValues->size().width() - d->treeviewFieldValues->verticalScrollBar()->size().width() - 8; d->treeviewFieldValues->setColumnWidth(0, widgetWidth - d->countWidth); d->treeviewFieldValues->setColumnWidth(1, d->countWidth); } void ValueList::listItemActivated(const QModelIndex &index) { setEnabled(false); QString itemText = d->sortingModel->mapToSource(index).data(ValueListModel::SearchTextRole).toString(); QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()); QString fieldText = fieldVar.toString(); if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText(); SortFilterFileModel::FilterQuery fq; fq.terms << itemText; - fq.combination = SortFilterFileModel::EveryTerm; + fq.combination = SortFilterFileModel::FilterCombination::EveryTerm; fq.field = fieldText; fq.searchPDFfiles = false; d->fileView->setFilterBarFilter(fq); setEnabled(true); } void ValueList::searchSelection() { QVariant fieldVar = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()); QString fieldText = fieldVar.toString(); if (fieldText.isEmpty()) fieldText = d->comboboxFieldNames->currentText(); SortFilterFileModel::FilterQuery fq; - fq.combination = SortFilterFileModel::EveryTerm; + fq.combination = SortFilterFileModel::FilterCombination::EveryTerm; fq.field = fieldText; const auto selectedIndexes = d->treeviewFieldValues->selectionModel()->selectedIndexes(); for (const QModelIndex &index : selectedIndexes) { if (index.column() == 0) { QString itemText = index.data(ValueListModel::SearchTextRole).toString(); fq.terms << itemText; } } fq.searchPDFfiles = false; if (!fq.terms.isEmpty()) d->fileView->setFilterBarFilter(fq); } void ValueList::assignSelection() { QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString(); if (field.isEmpty()) field = d->comboboxFieldNames->currentText(); if (field.isEmpty()) return; ///< empty field is invalid; quit const Value toBeAssignedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>(); if (toBeAssignedValue.isEmpty()) return; ///< empty value is invalid; quit const QString toBeAssignedValueText = PlainTextValue::text(toBeAssignedValue); /// Keep track if any modifications were made to the bibliography file bool madeModification = false; /// Go through all selected elements in current editor const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements(); for (const auto &element : selection) { /// Only entries (not macros or comments) are of interest QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { /// Fields are separated into two categories: /// 1. Where more values can be appended, like authors or URLs /// 2. Where values should be replaced, like title, year, or journal if (d->allowsMultipleValues(field)) { /// Fields for which multiple values are valid bool valueItemAlreadyContained = false; ///< add only if to-be-assigned value is not yet contained Value entrysValueForField = entry->value(field); for (const auto &containedValueItem : const_cast<const Value &>(entrysValueForField)) { valueItemAlreadyContained |= PlainTextValue::text(containedValueItem) == toBeAssignedValueText; if (valueItemAlreadyContained) break; } if (!valueItemAlreadyContained) { /// Add each ValueItem from the to-be-assigned value to the entry's value for this field entrysValueForField.reserve(toBeAssignedValue.size()); for (const auto &newValueItem : toBeAssignedValue) { entrysValueForField.append(newValueItem); } /// "Write back" value to field in entry entry->remove(field); entry->insert(field, entrysValueForField); /// Keep track that bibliography file has been modified madeModification = true; } } else { /// Fields for which only value is valid, thus the old value will be replaced entry->remove(field); entry->insert(field, toBeAssignedValue); /// Keep track that bibliography file has been modified madeModification = true; } } } if (madeModification) { /// Notify main editor about change it its data d->fileView->externalModification(); } } void ValueList::removeSelection() { QString field = d->comboboxFieldNames->itemData(d->comboboxFieldNames->currentIndex()).toString(); if (field.isEmpty()) field = d->comboboxFieldNames->currentText(); if (field.isEmpty()) return; ///< empty field is invalid; quit const Value toBeRemovedValue = d->sortingModel->mapToSource(d->treeviewFieldValues->currentIndex()).data(Qt::EditRole).value<Value>(); if (toBeRemovedValue.isEmpty()) return; ///< empty value is invalid; quit const QString toBeRemovedValueText = PlainTextValue::text(toBeRemovedValue); /// Keep track if any modifications were made to the bibliography file bool madeModification = false; /// Go through all selected elements in current editor const QList<QSharedPointer<Element> > &selection = d->fileView->selectedElements(); for (const auto &element : selection) { /// Only entries (not macros or comments) are of interest QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { Value entrysValueForField = entry->value(field); bool valueModified = false; for (int i = 0; i < entrysValueForField.count(); ++i) { const QString valueItemText = PlainTextValue::text(entrysValueForField[i]); if (valueItemText == toBeRemovedValueText) { valueModified = true; entrysValueForField.remove(i); break; } } if (valueModified) { entry->remove(field); entry->insert(field, entrysValueForField); madeModification = true; } } } if (madeModification) { update(); /// Notify main editor about change it its data d->fileView->externalModification(); } } void ValueList::startItemRenaming() { /// Get current index from sorted model QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex(); /// Make the tree view start and editing delegate on the index d->treeviewFieldValues->edit(sortedIndex); } void ValueList::deleteAllOccurrences() { /// Get current index from sorted model QModelIndex sortedIndex = d->treeviewFieldValues->currentIndex(); /// Get "real" index from original model, but resort to sibling in first column QModelIndex realIndex = d->sortingModel->mapToSource(sortedIndex); realIndex = realIndex.sibling(realIndex.row(), 0); /// Remove current index from data model d->model->removeValue(realIndex); /// Notify main editor about change it its data d->fileView->externalModification(); } void ValueList::showCountColumnToggled() { if (d->model != nullptr) d->model->setShowCountColumn(d->showCountColumnAction->isChecked()); if (d->showCountColumnAction->isChecked()) resizeEvent(nullptr); d->sortByCountAction->setEnabled(!d->showCountColumnAction->isChecked()); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeyShowCountColumn, d->showCountColumnAction->isChecked()); d->config->sync(); } void ValueList::sortByCountToggled() { if (d->model != nullptr) - d->model->setSortBy(d->sortByCountAction->isChecked() ? ValueListModel::SortByCount : ValueListModel::SortByText); + d->model->setSortBy(d->sortByCountAction->isChecked() ? ValueListModel::SortBy::Count : ValueListModel::SortBy::Text); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeySortByCountAction, d->sortByCountAction->isChecked()); d->config->sync(); } void ValueList::columnsChanged() { QByteArray headerState = d->treeviewFieldValues->header()->saveState(); KConfigGroup configGroup(d->config, d->configGroupName); configGroup.writeEntry(d->configKeyHeaderState, headerState); d->config->sync(); resizeEvent(nullptr); } void ValueList::editorSelectionChanged() { const bool selectedElements = d->fileView == nullptr ? false : d->fileView->selectedElements().count() > 0; d->assignSelectionAction->setEnabled(selectedElements); d->removeSelectionAction->setEnabled(selectedElements); } void ValueList::editorDestroyed() { /// Reset internal variable to NULL to avoid /// accessing invalid pointer/data later d->fileView = nullptr; editorSelectionChanged(); } void ValueList::fieldNamesChanged(int i) { const QString field = d->comboboxFieldNames->itemData(i).toString(); if (d->allowsMultipleValues(field)) d->assignSelectionAction->setText(i18n("Add value to selected entries")); else d->assignSelectionAction->setText(i18n("Replace value of selected entries")); update(); } diff --git a/src/program/docklets/zoterobrowser.cpp b/src/program/docklets/zoterobrowser.cpp index 23733d45..04b41d82 100644 --- a/src/program/docklets/zoterobrowser.cpp +++ b/src/program/docklets/zoterobrowser.cpp @@ -1,443 +1,443 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "zoterobrowser.h" #include <QTreeView> #include <QTabWidget> #include <QListView> #include <QLayout> #include <QFormLayout> #include <QAbstractItemModel> #include <QRadioButton> #include <QPushButton> #include <QPointer> #include <QLineEdit> #include <QComboBox> #include <KLocalizedString> #include <KWallet/KWallet> #include <KMessageBox> #include <Element> #include "searchresults.h" #include "zotero/collectionmodel.h" #include "zotero/collection.h" #include "zotero/items.h" #include "zotero/groups.h" #include "zotero/tags.h" #include "zotero/tagmodel.h" #include "zotero/api.h" #include "zotero/oauthwizard.h" #include "logging_program.h" using KWallet::Wallet; class ZoteroBrowser::Private { private: ZoteroBrowser *p; public: Zotero::Items *items; Zotero::Groups *groups; Zotero::Tags *tags; Zotero::TagModel *tagModel; Zotero::Collection *collection; Zotero::CollectionModel *collectionModel; QSharedPointer<Zotero::API> api; bool needToApplyCredentials; SearchResults *searchResults; QTabWidget *tabWidget; QTreeView *collectionBrowser; QListView *tagBrowser; QLineEdit *lineEditNumericUserId; QLineEdit *lineEditApiKey; QRadioButton *radioPersonalLibrary; QRadioButton *radioGroupLibrary; bool comboBoxGroupListInitialized; QComboBox *comboBoxGroupList; QCursor nonBusyCursor; Wallet *wallet; static const QString walletFolderOAuth, walletEntryKBibTeXZotero, walletKeyZoteroId, walletKeyZoteroApiKey; Private(SearchResults *sr, ZoteroBrowser *parent) : p(parent), items(nullptr), groups(nullptr), tags(nullptr), tagModel(nullptr), collection(nullptr), collectionModel(nullptr), needToApplyCredentials(true), searchResults(sr), comboBoxGroupListInitialized(false), nonBusyCursor(p->cursor()), wallet(nullptr) { setupGUI(); } ~Private() { if (wallet != nullptr) delete wallet; if (items != nullptr) delete items; if (groups != nullptr) delete groups; if (tags != nullptr) delete tags; if (tagModel != nullptr) delete tagModel; if (collection != nullptr) delete collection; if (collectionModel != nullptr) delete collectionModel; api.clear(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); tabWidget = new QTabWidget(p); layout->addWidget(tabWidget); QWidget *container = new QWidget(tabWidget); tabWidget->addTab(container, QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Library")); connect(tabWidget, &QTabWidget::currentChanged, p, &ZoteroBrowser::tabChanged); QBoxLayout *containerLayout = new QVBoxLayout(container); /// Personal or Group Library QGridLayout *gridLayout = new QGridLayout(); containerLayout->addLayout(gridLayout); gridLayout->setMargin(0); gridLayout->setColumnMinimumWidth(0, 16); // TODO determine size of a radio button radioPersonalLibrary = new QRadioButton(i18n("Personal library"), container); gridLayout->addWidget(radioPersonalLibrary, 0, 0, 1, 2); radioGroupLibrary = new QRadioButton(i18n("Group library"), container); gridLayout->addWidget(radioGroupLibrary, 1, 0, 1, 2); comboBoxGroupList = new QComboBox(container); gridLayout->addWidget(comboBoxGroupList, 2, 1, 1, 1); QSizePolicy sizePolicy = comboBoxGroupList->sizePolicy(); sizePolicy.setHorizontalPolicy(QSizePolicy::MinimumExpanding); comboBoxGroupList->setSizePolicy(sizePolicy); radioPersonalLibrary->setChecked(true); comboBoxGroupList->setEnabled(false); comboBoxGroupList->addItem(i18n("No groups available")); connect(radioGroupLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled); connect(radioPersonalLibrary, &QRadioButton::toggled, p, &ZoteroBrowser::radioButtonsToggled); connect(comboBoxGroupList, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, [this]() { needToApplyCredentials = true; }); containerLayout->addStretch(10); /// Credentials QFormLayout *containerForm = new QFormLayout(); containerLayout->addLayout(containerForm, 1); containerForm->setMargin(0); lineEditNumericUserId = new QLineEdit(container); lineEditNumericUserId->setSizePolicy(sizePolicy); lineEditNumericUserId->setReadOnly(true); containerForm->addRow(i18n("Numeric user id:"), lineEditNumericUserId); connect(lineEditNumericUserId, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList); lineEditApiKey = new QLineEdit(container); lineEditApiKey->setSizePolicy(sizePolicy); lineEditApiKey->setReadOnly(true); containerForm->addRow(i18n("API key:"), lineEditApiKey); connect(lineEditApiKey, &QLineEdit::textChanged, p, &ZoteroBrowser::invalidateGroupList); QBoxLayout *containerButtonLayout = new QHBoxLayout(); containerLayout->addLayout(containerButtonLayout, 0); containerButtonLayout->setMargin(0); QPushButton *buttonGetOAuthCredentials = new QPushButton(QIcon::fromTheme(QStringLiteral("preferences-web-browser-identification")), i18n("Get New Credentials"), container); containerButtonLayout->addWidget(buttonGetOAuthCredentials, 0); connect(buttonGetOAuthCredentials, &QPushButton::clicked, p, &ZoteroBrowser::getOAuthCredentials); containerButtonLayout->addStretch(1); /// Collection browser collectionBrowser = new QTreeView(tabWidget); tabWidget->addTab(collectionBrowser, QIcon::fromTheme(QStringLiteral("folder-yellow")), i18n("Collections")); collectionBrowser->setHeaderHidden(true); collectionBrowser->setExpandsOnDoubleClick(false); connect(collectionBrowser, &QTreeView::doubleClicked, p, &ZoteroBrowser::collectionDoubleClicked); /// Tag browser tagBrowser = new QListView(tabWidget); tabWidget->addTab(tagBrowser, QIcon::fromTheme(QStringLiteral("mail-tagged")), i18n("Tags")); connect(tagBrowser, &QListView::doubleClicked, p, &ZoteroBrowser::tagDoubleClicked); } void queueReadOAuthCredentials() { if (wallet != nullptr && wallet->isOpen()) p->readOAuthCredentials(true); else { /// Wallet is closed or not initialized if (wallet != nullptr) /// Delete existing but closed wallet, will be replaced by new, open wallet soon delete wallet; p->setEnabled(false); p->setCursor(Qt::WaitCursor); wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous); connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::readOAuthCredentials); } } void queueWriteOAuthCredentials() { if (wallet != nullptr && wallet->isOpen()) p->writeOAuthCredentials(true); else { /// Wallet is closed or not initialized if (wallet != nullptr) /// Delete existing but closed wallet, will be replaced by new, open wallet soon delete wallet; p->setEnabled(false); p->setCursor(Qt::WaitCursor); wallet = Wallet::openWallet(Wallet::NetworkWallet(), p->winId(), Wallet::Asynchronous); connect(wallet, &Wallet::walletOpened, p, &ZoteroBrowser::writeOAuthCredentials); } } }; const QString ZoteroBrowser::Private::walletFolderOAuth = QStringLiteral("OAuth"); const QString ZoteroBrowser::Private::walletEntryKBibTeXZotero = QStringLiteral("KBibTeX/Zotero"); const QString ZoteroBrowser::Private::walletKeyZoteroId = QStringLiteral("UserId"); const QString ZoteroBrowser::Private::walletKeyZoteroApiKey = QStringLiteral("ApiKey"); ZoteroBrowser::ZoteroBrowser(SearchResults *searchResults, QWidget *parent) : QWidget(parent), d(new ZoteroBrowser::Private(searchResults, this)) { /// Forece GUI update updateButtons(); radioButtonsToggled(); } ZoteroBrowser::~ZoteroBrowser() { delete d; } void ZoteroBrowser::visibiltyChanged(bool v) { if (v && d->lineEditApiKey->text().isEmpty()) /// If Zotero dock became visible and no API key is set, check KWallet for credentials d->queueReadOAuthCredentials(); } void ZoteroBrowser::modelReset() { if (!d->collection->busy() && !d->tags->busy()) { setCursor(d->nonBusyCursor); setEnabled(true); } else { setCursor(Qt::WaitCursor); setEnabled(false); } if (!d->tags->busy() && !d->collection->busy() && !(d->collection->initialized() && d->tags->initialized())) KMessageBox::information(this, i18n("KBibTeX failed to retrieve the bibliography from Zotero. Please check that the provided user id and API key are valid."), i18n("Failed to retrieve data from Zotero")); } void ZoteroBrowser::collectionDoubleClicked(const QModelIndex &index) { setCursor(Qt::WaitCursor); setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget) const QString collectionId = index.data(Zotero::CollectionModel::CollectionIdRole).toString(); d->searchResults->clear(); d->items->retrieveItemsByCollection(collectionId); } void ZoteroBrowser::tagDoubleClicked(const QModelIndex &index) { setCursor(Qt::WaitCursor); setEnabled(false); ///< will be re-enabled when item retrieve got finished (slot reenableWidget) const QString tag = index.data(Zotero::TagModel::TagRole).toString(); d->searchResults->clear(); d->items->retrieveItemsByTag(tag); } void ZoteroBrowser::showItem(QSharedPointer<Element> e) { d->searchResults->insertElement(e); emit itemToShow(); } void ZoteroBrowser::reenableWidget() { setCursor(d->nonBusyCursor); setEnabled(true); } void ZoteroBrowser::updateButtons() { const bool validNumericIdAndApiKey = !d->lineEditNumericUserId->text().isEmpty() && !d->lineEditApiKey->text().isEmpty(); d->radioGroupLibrary->setEnabled(validNumericIdAndApiKey); d->radioPersonalLibrary->setEnabled(validNumericIdAndApiKey); d->needToApplyCredentials = true; } bool ZoteroBrowser::applyCredentials() { bool ok = false; const int userId = d->lineEditNumericUserId->text().toInt(&ok); const QString apiKey = d->lineEditApiKey->text(); if (ok && !apiKey.isEmpty()) { setCursor(Qt::WaitCursor); setEnabled(false); ok = false; int groupId = d->comboBoxGroupList->itemData(d->comboBoxGroupList->currentIndex()).toInt(&ok); if (!ok) groupId = -1; disconnect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget); disconnect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget); disconnect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem); disconnect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset); disconnect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset); d->collection->deleteLater(); d->items->deleteLater(); d->tags->deleteLater(); d->collectionModel->deleteLater(); d->tagModel->deleteLater(); d->api.clear(); const bool makeGroupRequest = d->radioGroupLibrary->isChecked() && groupId > 0; - d->api = QSharedPointer<Zotero::API>(new Zotero::API(makeGroupRequest ? Zotero::API::GroupRequest : Zotero::API::UserRequest, makeGroupRequest ? groupId : userId, d->lineEditApiKey->text(), this)); + d->api = QSharedPointer<Zotero::API>(new Zotero::API(makeGroupRequest ? Zotero::API::RequestScope::Group : Zotero::API::RequestScope::User, makeGroupRequest ? groupId : userId, d->lineEditApiKey->text(), this)); d->items = new Zotero::Items(d->api, this); d->tags = new Zotero::Tags(d->api, this); d->tagModel = new Zotero::TagModel(d->tags, this); d->tagBrowser->setModel(d->tagModel); d->collection = new Zotero::Collection(d->api, this); d->collectionModel = new Zotero::CollectionModel(d->collection, this); d->collectionBrowser->setModel(d->collectionModel); connect(d->collectionModel, &Zotero::CollectionModel::modelReset, this, &ZoteroBrowser::modelReset); connect(d->tagModel, &Zotero::TagModel::modelReset, this, &ZoteroBrowser::modelReset); connect(d->items, &Zotero::Items::foundElement, this, &ZoteroBrowser::showItem); connect(d->items, &Zotero::Items::stoppedSearch, this, &ZoteroBrowser::reenableWidget); connect(d->tags, &Zotero::Tags::finishedLoading, this, &ZoteroBrowser::reenableWidget); d->needToApplyCredentials = false; return true; } else return false; } void ZoteroBrowser::radioButtonsToggled() { d->comboBoxGroupList->setEnabled(d->comboBoxGroupListInitialized && d->comboBoxGroupList->count() > 0 && d->radioGroupLibrary->isChecked()); if (!d->comboBoxGroupListInitialized && d->radioGroupLibrary->isChecked()) retrieveGroupList(); d->needToApplyCredentials = true; } void ZoteroBrowser::groupListChanged() { d->needToApplyCredentials = true; } void ZoteroBrowser::retrieveGroupList() { bool ok = false; const int userId = d->lineEditNumericUserId->text().toInt(&ok); if (ok) { setCursor(Qt::WaitCursor); setEnabled(false); d->comboBoxGroupList->clear(); d->comboBoxGroupListInitialized = false; disconnect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList); d->groups->deleteLater(); d->api.clear(); - d->api = QSharedPointer<Zotero::API>(new Zotero::API(Zotero::API::UserRequest, userId, d->lineEditApiKey->text(), this)); + d->api = QSharedPointer<Zotero::API>(new Zotero::API(Zotero::API::RequestScope::User, userId, d->lineEditApiKey->text(), this)); d->groups = new Zotero::Groups(d->api, this); connect(d->groups, &Zotero::Groups::finishedLoading, this, &ZoteroBrowser::gotGroupList); } } void ZoteroBrowser::invalidateGroupList() { d->comboBoxGroupList->clear(); d->comboBoxGroupListInitialized = false; d->comboBoxGroupList->addItem(i18n("No groups available or no permissions")); d->comboBoxGroupList->setEnabled(false); d->radioPersonalLibrary->setChecked(true); } void ZoteroBrowser::gotGroupList() { const QMap<int, QString> groupMap = d->groups->groups(); for (QMap<int, QString>::ConstIterator it = groupMap.constBegin(); it != groupMap.constEnd(); ++it) { d->comboBoxGroupList->addItem(it.value(), QVariant::fromValue<int>(it.key())); } if (groupMap.isEmpty()) { invalidateGroupList(); } else { d->comboBoxGroupListInitialized = true; d->comboBoxGroupList->setEnabled(true); d->needToApplyCredentials = true; } reenableWidget(); } void ZoteroBrowser::getOAuthCredentials() { QPointer<Zotero::OAuthWizard> wizard = new Zotero::OAuthWizard(this); if (wizard->exec() && !wizard->apiKey().isEmpty() && wizard->userId() >= 0) { d->lineEditApiKey->setText(wizard->apiKey()); d->lineEditNumericUserId->setText(QString::number(wizard->userId())); d->queueWriteOAuthCredentials(); updateButtons(); retrieveGroupList(); } delete wizard; } void ZoteroBrowser::readOAuthCredentials(bool ok) { /// Do not call this slot a second time disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::readOAuthCredentials); if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) { if (d->wallet->hasEntry(ZoteroBrowser::Private::walletEntryKBibTeXZotero)) { QMap<QString, QString> map; if (d->wallet->readMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) == 0) { if (map.contains(ZoteroBrowser::Private::walletKeyZoteroId) && map.contains(ZoteroBrowser::Private::walletKeyZoteroApiKey)) { d->lineEditNumericUserId->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroId, QString())); d->lineEditApiKey->setText(map.value(ZoteroBrowser::Private::walletKeyZoteroApiKey, QString())); updateButtons(); retrieveGroupList(); } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to locate Zotero Id and/or API key in KWallet"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Failed to access Zotero data in KWallet"; } else qCDebug(LOG_KBIBTEX_PROGRAM) << "No Zotero credentials stored in KWallet"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed"; reenableWidget(); } void ZoteroBrowser::writeOAuthCredentials(bool ok) { disconnect(d->wallet, &Wallet::walletOpened, this, &ZoteroBrowser::writeOAuthCredentials); if (ok && (d->wallet->hasFolder(ZoteroBrowser::Private::walletFolderOAuth) || d->wallet->createFolder(ZoteroBrowser::Private::walletFolderOAuth)) && d->wallet->setFolder(ZoteroBrowser::Private::walletFolderOAuth)) { QMap<QString, QString> map; map.insert(ZoteroBrowser::Private::walletKeyZoteroId, d->lineEditNumericUserId->text()); map.insert(ZoteroBrowser::Private::walletKeyZoteroApiKey, d->lineEditApiKey->text()); if (d->wallet->writeMap(ZoteroBrowser::Private::walletEntryKBibTeXZotero, map) != 0) qCWarning(LOG_KBIBTEX_PROGRAM) << "Writing API key to KWallet failed"; } else qCWarning(LOG_KBIBTEX_PROGRAM) << "Accessing KWallet to sync API key did not succeed"; reenableWidget(); } void ZoteroBrowser::tabChanged(int newTabIndex) { if (newTabIndex > 0 /** tabs after credential tab*/ && d->needToApplyCredentials) { const bool success = applyCredentials(); for (int i = 1; i < d->tabWidget->count(); ++i) d->tabWidget->widget(i)->setEnabled(success); } } diff --git a/src/program/documentlist.cpp b/src/program/documentlist.cpp index c949b0ec..b4c71f3b 100644 --- a/src/program/documentlist.cpp +++ b/src/program/documentlist.cpp @@ -1,420 +1,420 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "documentlist.h" #include <QStringListModel> #include <QTimer> #include <QGridLayout> #include <QLabel> #include <QPainter> #include <QPushButton> #include <QApplication> #include <QIcon> #include <QAction> #include <KIconLoader> #include <KLocalizedString> #include <KActionMenu> #include <KDirOperator> #include <KService> #include <KMessageBox> #include <KBibTeX> class DirOperatorWidget : public QWidget { Q_OBJECT public: KDirOperator *dirOperator; DirOperatorWidget(QWidget *parent) : QWidget(parent) { QGridLayout *layout = new QGridLayout(this); layout->setMargin(0); layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 0); layout->setColumnStretch(2, 1); QPushButton *buttonUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), this); buttonUp->setToolTip(i18n("One level up")); layout->addWidget(buttonUp, 0, 0, 1, 1); QPushButton *buttonHome = new QPushButton(QIcon::fromTheme(QStringLiteral("user-home")), QString(), this); buttonHome->setToolTip(i18n("Go to Home folder")); layout->addWidget(buttonHome, 0, 1, 1, 1); dirOperator = new KDirOperator(QUrl(QStringLiteral("file:") + QDir::homePath()), this); layout->addWidget(dirOperator, 1, 0, 1, 3); dirOperator->setView(KFile::Detail); connect(buttonUp, &QPushButton::clicked, dirOperator, &KDirOperator::cdUp); connect(buttonHome, &QPushButton::clicked, dirOperator, &KDirOperator::home); } }; DocumentListDelegate::DocumentListDelegate(QObject *parent) : QStyledItemDelegate(parent) { /// nothing } void DocumentListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { int height = option.rect.height(); QStyle *style = QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); painter->save(); if (option.state & QStyle::State_Selected) { painter->setPen(QPen(option.palette.highlightedText().color())); } else { painter->setPen(QPen(option.palette.text().color())); } OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(index.data(Qt::UserRole)); if (OpenFileInfoManager::instance().currentFile() == ofi) { /// for the currently open file, use a bold font to write file name QFont font = painter->font(); font.setBold(true); painter->setFont(font); } QRect textRect = option.rect; textRect.setLeft(textRect.left() + height + 4); textRect.setHeight(height / 2); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, index.data(Qt::DisplayRole).toString()); textRect = option.rect; textRect.setLeft(textRect.left() + height + 4); textRect.setTop(textRect.top() + height / 2); textRect.setHeight(height * 3 / 8); QFont font = painter->font(); font.setPointSize(font.pointSize() * 7 / 8); painter->setFont(font); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, ofi->fullCaption()); QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole)); painter->drawPixmap(option.rect.left() + 1, option.rect.top() + 1, height - 2, height - 2, icon.pixmap(height - 2, height - 2)); painter->restore(); } QSize DocumentListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(size.height() * 9 / 4); return size; } class DocumentListModel::DocumentListModelPrivate { public: OpenFileInfo::StatusFlag sf; OpenFileInfoManager::OpenFileInfoList ofiList; public: DocumentListModelPrivate(OpenFileInfo::StatusFlag statusFlag, DocumentListModel *parent) : sf(statusFlag) { Q_UNUSED(parent) } }; DocumentListModel::DocumentListModel(OpenFileInfo::StatusFlag statusFlag, QObject *parent) : QAbstractListModel(parent), d(new DocumentListModel::DocumentListModelPrivate(statusFlag, this)) { listsChanged(d->sf); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &DocumentListModel::listsChanged); } DocumentListModel::~DocumentListModel() { delete d; } int DocumentListModel::rowCount(const QModelIndex &parent) const { if (parent != QModelIndex()) return 0; return d->ofiList.count(); } QVariant DocumentListModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= rowCount()) return QVariant(); OpenFileInfo *openFileInfo = d->ofiList[index.row()]; const QString iconName = openFileInfo->mimeType().replace(QLatin1Char('/'), QLatin1Char('-')); switch (role) { case Qt::DisplayRole: return openFileInfo->shortCaption(); case Qt::DecorationRole: { /// determine mime type-based icon and overlays (e.g. for modified files) QStringList overlays; - if (openFileInfo->flags().testFlag(OpenFileInfo::Favorite)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Favorite)) overlays << QStringLiteral("favorites"); else overlays << QString(); - if (openFileInfo->flags().testFlag(OpenFileInfo::RecentlyUsed)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::RecentlyUsed)) overlays << QStringLiteral("document-open-recent"); else overlays << QString(); - if (openFileInfo->flags().testFlag(OpenFileInfo::Open)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open)) overlays << QStringLiteral("folder-open"); else overlays << QString(); if (openFileInfo->isModified()) overlays << QStringLiteral("document-save"); else overlays << QString(); return KDE::icon(iconName, overlays, nullptr); } case Qt::ToolTipRole: { QString htmlText(QString(QStringLiteral("<qt><img src=\"%1\"> <b>%2</b>")).arg(KIconLoader::global()->iconPath(iconName, KIconLoader::Small), openFileInfo->shortCaption())); const QUrl url = openFileInfo->url(); if (url.isValid()) { QString path(QFileInfo(url.path()).path()); if (!path.endsWith(QLatin1Char('/'))) path.append(QLatin1Char('/')); htmlText.append(i18n("<br/><small>located in <b>%1</b></small>", path)); } QStringList flagListItems; - if (openFileInfo->flags().testFlag(OpenFileInfo::Favorite)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Favorite)) flagListItems << i18n("Favorite"); - if (openFileInfo->flags().testFlag(OpenFileInfo::RecentlyUsed)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::RecentlyUsed)) flagListItems << i18n("Recently Used"); - if (openFileInfo->flags().testFlag(OpenFileInfo::Open)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open)) flagListItems << i18n("Open"); if (openFileInfo->isModified()) flagListItems << i18n("Modified"); if (!flagListItems.empty()) { htmlText.append(QStringLiteral("<ul>")); for (const QString &flagListItem : const_cast<const QStringList &>(flagListItems)) { htmlText.append(QString(QStringLiteral("<li>%1</li>")).arg(flagListItem)); } htmlText.append(QStringLiteral("</ul>")); } htmlText.append(QStringLiteral("</qt>")); return htmlText; } case Qt::UserRole: return qVariantFromValue(openFileInfo); default: return QVariant(); } } QVariant DocumentListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || section != 0 || role != Qt::DisplayRole) return QVariant(); return QVariant("List of Files"); } void DocumentListModel::listsChanged(OpenFileInfo::StatusFlags statusFlags) { if (statusFlags.testFlag(d->sf)) { beginResetModel(); d->ofiList = OpenFileInfoManager::instance().filteredItems(d->sf); endResetModel(); } } class DocumentListView::DocumentListViewPrivate { private: DocumentListView *p; public: QAction *actionAddToFav, *actionRemFromFav; QAction *actionCloseFile, *actionOpenFile; KActionMenu *actionOpenMenu; QList<QAction *> openMenuActions; DocumentListViewPrivate(DocumentListView *parent) : p(parent), actionAddToFav(nullptr), actionRemFromFav(nullptr), actionCloseFile(nullptr), actionOpenFile(nullptr), actionOpenMenu(nullptr) { /// nothing } void openFileWithService(KService::Ptr service) { const QModelIndex modelIndex = p->currentIndex(); if (modelIndex != QModelIndex()) { OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole)); if (!ofi->isModified() || (KMessageBox::questionYesNo(p, i18n("The current document has to be saved before switching the viewer/editor component."), i18n("Save before switching?"), KGuiItem(i18n("Save document"), QIcon::fromTheme(QStringLiteral("document-save"))), KGuiItem(i18n("Do not switch"), QIcon::fromTheme(QStringLiteral("dialog-cancel")))) == KMessageBox::Yes && ofi->save())) OpenFileInfoManager::instance().setCurrentFile(ofi, service); } } }; DocumentListView::DocumentListView(OpenFileInfo::StatusFlag statusFlag, QWidget *parent) : QListView(parent), d(new DocumentListViewPrivate(this)) { setContextMenuPolicy(Qt::ActionsContextMenu); setItemDelegate(new DocumentListDelegate(this)); - if (statusFlag == OpenFileInfo::Open) { + if (statusFlag == OpenFileInfo::StatusFlag::Open) { d->actionCloseFile = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18n("Close File"), this); connect(d->actionCloseFile, &QAction::triggered, this, &DocumentListView::closeFile); addAction(d->actionCloseFile); } else { d->actionOpenFile = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open File"), this); connect(d->actionOpenFile, &QAction::triggered, this, &DocumentListView::openFile); addAction(d->actionOpenFile); } d->actionOpenMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open with"), this); addAction(d->actionOpenMenu); - if (statusFlag == OpenFileInfo::Favorite) { + if (statusFlag == OpenFileInfo::StatusFlag::Favorite) { d->actionRemFromFav = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Remove from Favorites"), this); connect(d->actionRemFromFav, &QAction::triggered, this, &DocumentListView::removeFromFavorites); addAction(d->actionRemFromFav); } else { d->actionAddToFav = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Add to Favorites"), this); connect(d->actionAddToFav, &QAction::triggered, this, &DocumentListView::addToFavorites); addAction(d->actionAddToFav); } connect(this, &DocumentListView::activated, this, &DocumentListView::openFile); currentChanged(QModelIndex(), QModelIndex()); } DocumentListView::~DocumentListView() { delete d; } void DocumentListView::addToFavorites() { QModelIndex modelIndex = currentIndex(); if (modelIndex != QModelIndex()) { OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole)); - ofi->addFlags(OpenFileInfo::Favorite); + ofi->addFlags(OpenFileInfo::StatusFlag::Favorite); } } void DocumentListView::removeFromFavorites() { QModelIndex modelIndex = currentIndex(); if (modelIndex != QModelIndex()) { OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole)); - ofi->removeFlags(OpenFileInfo::Favorite); + ofi->removeFlags(OpenFileInfo::StatusFlag::Favorite); } } void DocumentListView::openFile() { QModelIndex modelIndex = currentIndex(); if (modelIndex != QModelIndex()) { OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole)); OpenFileInfoManager::instance().setCurrentFile(ofi); } } void DocumentListView::closeFile() { QModelIndex modelIndex = currentIndex(); if (modelIndex != QModelIndex()) { OpenFileInfo *ofi = qvariant_cast<OpenFileInfo *>(modelIndex.data(Qt::UserRole)); OpenFileInfoManager::instance().close(ofi); } } void DocumentListView::currentChanged(const QModelIndex &current, const QModelIndex &) { bool hasCurrent = current != QModelIndex(); OpenFileInfo *ofi = hasCurrent ? qvariant_cast<OpenFileInfo *>(current.data(Qt::UserRole)) : nullptr; - bool isOpen = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::Open) : false; - bool isFavorite = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::Favorite) : false; - bool hasName = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::HasName) : false; + bool isOpen = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::Open) : false; + bool isFavorite = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::Favorite) : false; + bool hasName = hasCurrent ? ofi->flags().testFlag(OpenFileInfo::StatusFlag::HasName) : false; if (d->actionOpenFile != nullptr) d->actionOpenFile->setEnabled(hasCurrent && !isOpen); if (d->actionCloseFile != nullptr) d->actionCloseFile->setEnabled(hasCurrent && isOpen); if (d->actionAddToFav != nullptr) d->actionAddToFav->setEnabled(hasCurrent && !isFavorite && hasName); if (d->actionRemFromFav != nullptr) d->actionRemFromFav->setEnabled(hasCurrent && isFavorite); for (QAction *action : const_cast<const QList<QAction *> &>(d->openMenuActions)) { d->actionOpenMenu->removeAction(action); } if (ofi != nullptr) { const KService::List services = ofi->listOfServices(); for (KService::Ptr servicePtr : services) { QAction *menuItem = new QAction(QIcon::fromTheme(servicePtr->icon()), servicePtr->name(), this); d->actionOpenMenu->addAction(menuItem); d->openMenuActions << menuItem; connect(menuItem, &QAction::triggered, this, [this, servicePtr]() { d->openFileWithService(servicePtr); }); } } d->actionOpenMenu->setEnabled(!d->openMenuActions.isEmpty()); } class DocumentList::DocumentListPrivate { public: DocumentListView *listOpenFiles; DocumentListView *listRecentFiles; DocumentListView *listFavorites; DirOperatorWidget *dirOperator; DocumentListPrivate(DocumentList *p) { - listOpenFiles = new DocumentListView(OpenFileInfo::Open, p); - DocumentListModel *model = new DocumentListModel(OpenFileInfo::Open, listOpenFiles); + listOpenFiles = new DocumentListView(OpenFileInfo::StatusFlag::Open, p); + DocumentListModel *model = new DocumentListModel(OpenFileInfo::StatusFlag::Open, listOpenFiles); listOpenFiles->setModel(model); p->addTab(listOpenFiles, QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Files")); - listRecentFiles = new DocumentListView(OpenFileInfo::RecentlyUsed, p); - model = new DocumentListModel(OpenFileInfo::RecentlyUsed, listRecentFiles); + listRecentFiles = new DocumentListView(OpenFileInfo::StatusFlag::RecentlyUsed, p); + model = new DocumentListModel(OpenFileInfo::StatusFlag::RecentlyUsed, listRecentFiles); listRecentFiles->setModel(model); p->addTab(listRecentFiles, QIcon::fromTheme(QStringLiteral("document-open-recent")), i18n("Recently Used")); - listFavorites = new DocumentListView(OpenFileInfo::Favorite, p); - model = new DocumentListModel(OpenFileInfo::Favorite, listFavorites); + listFavorites = new DocumentListView(OpenFileInfo::StatusFlag::Favorite, p); + model = new DocumentListModel(OpenFileInfo::StatusFlag::Favorite, listFavorites); listFavorites->setModel(model); p->addTab(listFavorites, QIcon::fromTheme(QStringLiteral("favorites")), i18n("Favorites")); dirOperator = new DirOperatorWidget(p); p->addTab(dirOperator, QIcon::fromTheme(QStringLiteral("system-file-manager")), i18n("Filesystem Browser")); connect(dirOperator->dirOperator, &KDirOperator::fileSelected, p, &DocumentList::fileSelected); } }; DocumentList::DocumentList(QWidget *parent) : QTabWidget(parent), d(new DocumentListPrivate(this)) { setDocumentMode(true); } void DocumentList::fileSelected(const KFileItem &item) { if (item.isFile() && item.isReadable()) emit openFile(item.url()); } #include "documentlist.moc" diff --git a/src/program/documentlist.h b/src/program/documentlist.h index f1c678c5..b3132313 100644 --- a/src/program/documentlist.h +++ b/src/program/documentlist.h @@ -1,108 +1,108 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_DOCUMENTLIST_H #define KBIBTEX_PROGRAM_DOCUMENTLIST_H #include <QListView> #include <QAbstractListModel> #include <QListWidgetItem> #include <QStyledItemDelegate> #include <QTabWidget> #include <QUrl> #include "openfileinfo.h" class KFileItem; class OpenFileInfoManager; class DocumentListDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit DocumentListDelegate(QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; class DocumentListModel : public QAbstractListModel { Q_OBJECT public: explicit DocumentListModel(OpenFileInfo::StatusFlag statusFlag, QObject *parent = nullptr); ~DocumentListModel() override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; private: class DocumentListModelPrivate; DocumentListModelPrivate *d; private slots: void listsChanged(OpenFileInfo::StatusFlags statusFlags); }; class DocumentListView : public QListView { Q_OBJECT public: DocumentListView(OpenFileInfo::StatusFlag statusFlag, QWidget *parent); ~DocumentListView() override; private slots: void addToFavorites(); void removeFromFavorites(); void openFile(); void closeFile(); protected: void currentChanged(const QModelIndex &current, const QModelIndex &previous) override; private: class DocumentListViewPrivate; DocumentListViewPrivate *d; }; class DocumentList : public QTabWidget { Q_OBJECT public: - enum Category { OpenFiles = 0, RecentFiles = 1, Favorites = 2 }; + enum class Category { OpenFiles = 0, RecentFiles = 1, Favorites = 2 }; explicit DocumentList(QWidget *parent = nullptr); signals: void openFile(const QUrl &url); private slots: void fileSelected(const KFileItem &item); private: class DocumentListPrivate; DocumentListPrivate *d; }; #endif // KBIBTEX_PROGRAM_DOCUMENTLIST_H diff --git a/src/program/mainwindow.cpp b/src/program/mainwindow.cpp index f0c46218..f72d16af 100644 --- a/src/program/mainwindow.cpp +++ b/src/program/mainwindow.cpp @@ -1,478 +1,478 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "mainwindow.h" #include <QDockWidget> #include <QDragEnterEvent> #include <QDropEvent> #include <QLabel> #include <QMimeData> #include <QPointer> #include <QMenu> #include <QTimer> #include <QApplication> #include <QFileDialog> #include <QAction> #include <KActionMenu> #include <KActionCollection> #include <KPluginFactory> #include <KPluginLoader> #include <KLocalizedString> #include <KMessageBox> #include <KBibTeX> #include <preferences/KBibTeXPreferencesDialog> #include <file/FileView> #include <XSLTransform> #include <BibliographyService> #include <BibUtils> #include "docklets/referencepreview.h" #include "docklets/documentpreview.h" #include "docklets/searchform.h" #include "docklets/searchresults.h" #include "docklets/elementform.h" #include "docklets/documentpreview.h" #include "docklets/statistics.h" #include "docklets/filesettings.h" #include "docklets/valuelist.h" #include "docklets/zoterobrowser.h" #include "documentlist.h" #include "mdiwidget.h" class KBibTeXMainWindow::KBibTeXMainWindowPrivate { private: KBibTeXMainWindow *p; public: QAction *actionClose; QDockWidget *dockDocumentList; QDockWidget *dockReferencePreview; QDockWidget *dockDocumentPreview; QDockWidget *dockValueList; QDockWidget *dockZotero; QDockWidget *dockStatistics; QDockWidget *dockSearchForm; QDockWidget *dockSearchResults; QDockWidget *dockElementForm; QDockWidget *dockFileSettings; DocumentList *listDocumentList; MDIWidget *mdiWidget; ReferencePreview *referencePreview; DocumentPreview *documentPreview; FileSettings *fileSettings; ValueList *valueList; ZoteroBrowser *zotero; Statistics *statistics; SearchForm *searchForm; SearchResults *searchResults; ElementForm *elementForm; QMenu *actionMenuRecentFilesMenu; KBibTeXMainWindowPrivate(KBibTeXMainWindow *parent) : p(parent) { mdiWidget = new MDIWidget(p); KActionMenu *showPanelsAction = new KActionMenu(i18n("Show Panels"), p); p->actionCollection()->addAction(QStringLiteral("settings_shown_panels"), showPanelsAction); QMenu *showPanelsMenu = new QMenu(showPanelsAction->text(), p->widget()); showPanelsAction->setMenu(showPanelsMenu); KActionMenu *actionMenuRecentFiles = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-open-recent")), i18n("Recently used files"), p); p->actionCollection()->addAction(QStringLiteral("file_open_recent"), actionMenuRecentFiles); actionMenuRecentFilesMenu = new QMenu(actionMenuRecentFiles->text(), p->widget()); actionMenuRecentFiles->setMenu(actionMenuRecentFilesMenu); /** * Docklets (a.k.a. panels) will be added by default to the following * positions unless otherwise configured by the user. * - "List of Values" on the left * - "Statistics" on the left * - "List of Documents" on the left in the same tab * - "Online Search" on the left in a new tab * - "Reference Preview" on the left in the same tab * - "Search Results" on the bottom * - "Document Preview" is hidden * - "Element Editor" is hidden */ dockDocumentList = new QDockWidget(i18n("List of Documents"), p); dockDocumentList->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockDocumentList); listDocumentList = new DocumentList(dockDocumentList); dockDocumentList->setWidget(listDocumentList); dockDocumentList->setObjectName(QStringLiteral("dockDocumentList")); dockDocumentList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); connect(listDocumentList, &DocumentList::openFile, p, &KBibTeXMainWindow::openDocument); showPanelsMenu->addAction(dockDocumentList->toggleViewAction()); dockValueList = new QDockWidget(i18n("List of Values"), p); dockValueList->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockValueList); valueList = new ValueList(dockValueList); dockValueList->setWidget(valueList); dockValueList->setObjectName(QStringLiteral("dockValueList")); dockValueList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockValueList->toggleViewAction()); dockStatistics = new QDockWidget(i18n("Statistics"), p); dockStatistics->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockStatistics); statistics = new Statistics(dockStatistics); dockStatistics->setWidget(statistics); dockStatistics->setObjectName(QStringLiteral("dockStatistics")); dockStatistics->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockStatistics->toggleViewAction()); dockSearchResults = new QDockWidget(i18n("Search Results"), p); dockSearchResults->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockSearchResults); dockSearchResults->hide(); searchResults = new SearchResults(mdiWidget, dockSearchResults); dockSearchResults->setWidget(searchResults); dockSearchResults->setObjectName(QStringLiteral("dockResultsFrom")); dockSearchResults->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchResults->toggleViewAction()); connect(mdiWidget, &MDIWidget::documentSwitched, searchResults, &SearchResults::documentSwitched); dockSearchForm = new QDockWidget(i18n("Online Search"), p); dockSearchForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockSearchForm); searchForm = new SearchForm(searchResults, dockSearchForm); connect(searchForm, &SearchForm::doneSearching, p, &KBibTeXMainWindow::showSearchResults); dockSearchForm->setWidget(searchForm); dockSearchForm->setObjectName(QStringLiteral("dockSearchFrom")); dockSearchForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchForm->toggleViewAction()); dockZotero = new QDockWidget(i18n("Zotero"), p); dockZotero->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockZotero); zotero = new ZoteroBrowser(searchResults, dockZotero); connect(dockZotero, &QDockWidget::visibilityChanged, zotero, &ZoteroBrowser::visibiltyChanged); connect(zotero, &ZoteroBrowser::itemToShow, p, &KBibTeXMainWindow::showSearchResults); dockZotero->setWidget(zotero); dockZotero->setObjectName(QStringLiteral("dockZotero")); dockZotero->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockZotero->toggleViewAction()); dockReferencePreview = new QDockWidget(i18n("Reference Preview"), p); dockReferencePreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockReferencePreview); referencePreview = new ReferencePreview(dockReferencePreview); dockReferencePreview->setWidget(referencePreview); dockReferencePreview->setObjectName(QStringLiteral("dockReferencePreview")); dockReferencePreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockReferencePreview->toggleViewAction()); dockDocumentPreview = new QDockWidget(i18n("Document Preview"), p); dockDocumentPreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::RightDockWidgetArea, dockDocumentPreview); dockDocumentPreview->hide(); documentPreview = new DocumentPreview(dockDocumentPreview); dockDocumentPreview->setWidget(documentPreview); dockDocumentPreview->setObjectName(QStringLiteral("dockDocumentPreview")); dockDocumentPreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockDocumentPreview->toggleViewAction()); p->actionCollection()->setDefaultShortcut(dockDocumentPreview->toggleViewAction(), Qt::CTRL + Qt::SHIFT + Qt::Key_D); dockElementForm = new QDockWidget(i18n("Element Editor"), p); dockElementForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockElementForm); dockElementForm->hide(); elementForm = new ElementForm(mdiWidget, dockElementForm); dockElementForm->setWidget(elementForm); dockElementForm->setObjectName(QStringLiteral("dockElementFrom")); dockElementForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockElementForm->toggleViewAction()); dockFileSettings = new QDockWidget(i18n("File Settings"), p); dockFileSettings->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockFileSettings); fileSettings = new FileSettings(dockFileSettings); dockFileSettings->setWidget(fileSettings); dockFileSettings->setObjectName(QStringLiteral("dockFileSettings")); dockFileSettings->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockFileSettings->toggleViewAction()); p->tabifyDockWidget(dockFileSettings, dockSearchForm); p->tabifyDockWidget(dockZotero, dockSearchForm); p->tabifyDockWidget(dockValueList, dockStatistics); p->tabifyDockWidget(dockStatistics, dockFileSettings); p->tabifyDockWidget(dockSearchForm, dockReferencePreview); p->tabifyDockWidget(dockFileSettings, dockDocumentList); QAction *action = p->actionCollection()->addAction(KStandardAction::New); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::newDocument); action = p->actionCollection()->addAction(KStandardAction::Open); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::openDocumentDialog); actionClose = p->actionCollection()->addAction(KStandardAction::Close); connect(actionClose, &QAction::triggered, p, []() { OpenFileInfoManager::instance().close(OpenFileInfoManager::instance().currentFile()); }); actionClose->setEnabled(false); action = p->actionCollection()->addAction(KStandardAction::Quit); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::queryCloseAll); action = p->actionCollection()->addAction(KStandardAction::Preferences); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::showPreferences); } ~KBibTeXMainWindowPrivate() { elementForm->deleteLater(); delete mdiWidget; // TODO other deletes } }; KBibTeXMainWindow::KBibTeXMainWindow(QWidget *parent) : KParts::MainWindow(parent, static_cast<Qt::WindowFlags>(KDE_DEFAULT_WINDOWFLAGS)), d(new KBibTeXMainWindowPrivate(this)) { setObjectName(QStringLiteral("KBibTeXShell")); setXMLFile(QStringLiteral("kbibtexui.rc")); setCentralWidget(d->mdiWidget); connect(d->mdiWidget, &MDIWidget::documentSwitched, this, &KBibTeXMainWindow::documentSwitched); connect(d->mdiWidget, &MDIWidget::activePartChanged, this, &KBibTeXMainWindow::createGUI); ///< actually: KParts::MainWindow::createGUI connect(d->mdiWidget, &MDIWidget::documentNew, this, &KBibTeXMainWindow::newDocument); connect(d->mdiWidget, &MDIWidget::documentOpen, this, &KBibTeXMainWindow::openDocumentDialog); connect(d->mdiWidget, &MDIWidget::documentOpenURL, this, &KBibTeXMainWindow::openDocument); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::currentChanged, d->mdiWidget, &MDIWidget::setFile); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &KBibTeXMainWindow::documentListsChanged); connect(d->mdiWidget, &MDIWidget::setCaption, this, static_cast<void(KMainWindow::*)(const QString &)>(&KMainWindow::setCaption)); ///< actually: KMainWindow::setCaption - documentListsChanged(OpenFileInfo::RecentlyUsed); /// force initialization of menu of recently used files + documentListsChanged(OpenFileInfo::StatusFlag::RecentlyUsed); /// force initialization of menu of recently used files setupControllers(); setupGUI(KXmlGuiWindow::Create | KXmlGuiWindow::Save | KXmlGuiWindow::Keys | KXmlGuiWindow::ToolBar); setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); setAcceptDrops(true); QTimer::singleShot(500, this, &KBibTeXMainWindow::delayed); } KBibTeXMainWindow::~KBibTeXMainWindow() { delete d; } void KBibTeXMainWindow::setupControllers() { // TODO } void KBibTeXMainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); } void KBibTeXMainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urlList = event->mimeData()->urls(); if (urlList.isEmpty()) { const QUrl url(event->mimeData()->text()); if (url.isValid()) urlList << url; } if (!urlList.isEmpty()) for (const QUrl &url : const_cast<const QList<QUrl> &>(urlList)) openDocument(url); } void KBibTeXMainWindow::newDocument() { const QString mimeType = FileInfo::mimetypeBibTeX; OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().createNew(mimeType); if (openFileInfo) OpenFileInfoManager::instance().setCurrentFile(openFileInfo); else KMessageBox::error(this, i18n("Creating a new document of mime type '%1' failed as no editor component could be instantiated.", mimeType), i18n("Creating document failed")); } void KBibTeXMainWindow::openDocumentDialog() { OpenFileInfo *currFile = OpenFileInfoManager::instance().currentFile(); QUrl currFileUrl = currFile == nullptr ? QUrl() : currFile->url(); QString startDir = currFileUrl.isValid() ? QUrl(currFileUrl.url()).path() : QString(); OpenFileInfo *ofi = OpenFileInfoManager::instance().currentFile(); if (ofi != nullptr) { QUrl url = ofi->url(); if (url.isValid()) startDir = url.path(); } /// Assemble list of supported mimetypes QStringList supportedMimeTypes {QStringLiteral("text/x-bibtex"), QStringLiteral("application/x-research-info-systems"), QStringLiteral("application/xml")}; if (BibUtils::available()) { supportedMimeTypes.append(QStringLiteral("application/x-isi-export-format")); supportedMimeTypes.append(QStringLiteral("application/x-endnote-refer")); } supportedMimeTypes.append(QStringLiteral("application/pdf")); supportedMimeTypes.append(QStringLiteral("all/all")); QPointer<QFileDialog> dlg = new QFileDialog(this, i18n("Open file") /* TODO better text */, startDir); dlg->setMimeTypeFilters(supportedMimeTypes); dlg->setFileMode(QFileDialog::ExistingFile); const bool dialogAccepted = dlg->exec() != 0; const QUrl url = (dialogAccepted && !dlg->selectedUrls().isEmpty()) ? dlg->selectedUrls().first() : QUrl(); delete dlg; if (url.isValid()) openDocument(url); } void KBibTeXMainWindow::openDocument(const QUrl &url) { OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().open(url); OpenFileInfoManager::instance().setCurrentFile(openFileInfo); } void KBibTeXMainWindow::closeEvent(QCloseEvent *event) { KMainWindow::closeEvent(event); if (OpenFileInfoManager::instance().queryCloseAll()) event->accept(); else event->ignore(); } void KBibTeXMainWindow::showPreferences() { QPointer<KBibTeXPreferencesDialog> dlg = new KBibTeXPreferencesDialog(this); dlg->exec(); delete dlg; } void KBibTeXMainWindow::documentSwitched(FileView *oldFileView, FileView *newFileView) { OpenFileInfo *openFileInfo = d->mdiWidget->currentFile(); bool validFile = openFileInfo != nullptr; d->actionClose->setEnabled(validFile); setCaption(validFile ? i18n("%1 - KBibTeX", openFileInfo->shortCaption()) : i18n("KBibTeX")); d->fileSettings->setEnabled(newFileView != nullptr); d->referencePreview->setEnabled(newFileView != nullptr); d->elementForm->setEnabled(newFileView != nullptr); d->documentPreview->setEnabled(newFileView != nullptr); if (oldFileView != nullptr) { disconnect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); disconnect(newFileView, &FileView::modified, d->valueList, &ValueList::update); disconnect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME disconnect(oldEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); disconnect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } if (newFileView != nullptr) { connect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); connect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); connect(newFileView, &FileView::modified, d->valueList, &ValueList::update); connect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME connect(newEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } d->documentPreview->setBibTeXUrl(validFile ? openFileInfo->url() : QUrl()); d->referencePreview->setElement(QSharedPointer<Element>(), nullptr); d->elementForm->setElement(QSharedPointer<Element>(), nullptr); d->documentPreview->setElement(QSharedPointer<Element>(), nullptr); d->valueList->setFileView(newFileView); d->fileSettings->setFileView(newFileView); d->statistics->setFileView(newFileView); d->referencePreview->setFileView(newFileView); } void KBibTeXMainWindow::showSearchResults() { d->dockSearchResults->show(); } void KBibTeXMainWindow::documentListsChanged(OpenFileInfo::StatusFlags statusFlags) { - if (statusFlags.testFlag(OpenFileInfo::RecentlyUsed)) { - const OpenFileInfoManager::OpenFileInfoList list = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::RecentlyUsed); + if (statusFlags.testFlag(OpenFileInfo::StatusFlag::RecentlyUsed)) { + const OpenFileInfoManager::OpenFileInfoList list = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::StatusFlag::RecentlyUsed); d->actionMenuRecentFilesMenu->clear(); for (OpenFileInfo *cur : list) { /// Fixing bug 19511: too long filenames make menu too large, /// therefore squeeze text if it is longer than squeezeLen. const int squeezeLen = 64; const QString squeezedShortCap = squeeze_text(cur->shortCaption(), squeezeLen); const QString squeezedFullCap = squeeze_text(cur->fullCaption(), squeezeLen); QAction *action = new QAction(QString(QStringLiteral("%1 [%2]")).arg(squeezedShortCap, squeezedFullCap), this); action->setData(cur->url()); action->setIcon(QIcon::fromTheme(cur->mimeType().replace(QLatin1Char('/'), QLatin1Char('-')))); d->actionMenuRecentFilesMenu->addAction(action); connect(action, &QAction::triggered, this, &KBibTeXMainWindow::openRecentFile); } } } void KBibTeXMainWindow::openRecentFile() { QAction *action = static_cast<QAction *>(sender()); QUrl url = action->data().toUrl(); openDocument(url); } void KBibTeXMainWindow::queryCloseAll() { if (OpenFileInfoManager::instance().queryCloseAll()) qApp->quit(); } void KBibTeXMainWindow::delayed() { /// Static variable, memorizes the dynamically created /// BibliographyService instance and allows to tell if /// this slot was called for the first or second time. static BibliographyService *bs = nullptr; if (bs == nullptr) { /// First call to this slot bs = new BibliographyService(this); if (!bs->isKBibTeXdefault() && KMessageBox::questionYesNo(this, i18n("KBibTeX is not the default editor for its bibliography formats like BibTeX or RIS."), i18n("Default Bibliography Editor"), KGuiItem(i18n("Set as Default Editor")), KGuiItem(i18n("Keep settings unchanged"))) == KMessageBox::Yes) { bs->setKBibTeXasDefault(); /// QTimer calls this slot again, but as 'bs' will not be NULL, /// the 'if' construct's 'else' path will be followed. QTimer::singleShot(5000, this, &KBibTeXMainWindow::delayed); } else { /// KBibTeX is default application or user doesn't care, /// therefore clean up memory delete bs; bs = nullptr; } } else { /// Second call to this slot. This time, clean up memory. bs->deleteLater(); bs = nullptr; } } diff --git a/src/program/mdiwidget.cpp b/src/program/mdiwidget.cpp index a97024b3..9566886d 100644 --- a/src/program/mdiwidget.cpp +++ b/src/program/mdiwidget.cpp @@ -1,338 +1,338 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "mdiwidget.h" #include <QVector> #include <QPair> #include <QLabel> #include <QApplication> #include <QLayout> #include <QTreeView> #include <QAbstractItemModel> #include <QSortFilterProxyModel> #include <QHeaderView> #include <QPushButton> #include <QIcon> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <KConfigGroup> #include <KSharedConfig> #include <KLocalizedString> #include <KMessageBox> #include <KBibTeX> #include <file/PartWidget> #include "logging_program.h" class LRUItemModel : public QAbstractItemModel { Q_OBJECT public: static const int URLRole = Qt::UserRole + 235; static const int SortRole = Qt::UserRole + 236; LRUItemModel(QObject *parent = nullptr) : QAbstractItemModel(parent) { /// nothing } void reset() { beginResetModel(); endResetModel(); } int columnCount(const QModelIndex &) const override { return 3; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { - OpenFileInfoManager::OpenFileInfoList ofiList = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::RecentlyUsed); + OpenFileInfoManager::OpenFileInfoList ofiList = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::StatusFlag::RecentlyUsed); if (index.row() < ofiList.count()) { OpenFileInfo *ofiItem = ofiList[index.row()]; if (index.column() == 0) { if (role == Qt::DisplayRole || role == SortRole) { const QUrl url = ofiItem->url(); const QString fileName = url.fileName(); return fileName.isEmpty() ? squeeze_text(url.url(QUrl::PreferLocalFile), 32) : fileName; } else if (role == Qt::DecorationRole) return QIcon::fromTheme(ofiItem->mimeType().replace(QLatin1Char('/'), QLatin1Char('-'))); else if (role == Qt::ToolTipRole) return squeeze_text(ofiItem->url().url(QUrl::PreferLocalFile), 64); } else if (index.column() == 1) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) return ofiItem->lastAccess().toString(Qt::TextDate); else if (role == SortRole) return ofiItem->lastAccess().toTime_t(); } else if (index.column() == 2) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole || role == SortRole) return ofiItem->url().url(QUrl::PreferLocalFile); } if (role == URLRole) return ofiItem->url(); } return QVariant(); } QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override { if (role != Qt::DisplayRole || section > 2) return QVariant(); else if (section == 0) return i18n("Filename"); else if (section == 1) return i18n("Date/time of last use"); else return i18n("Full filename"); } QModelIndex index(int row, int column, const QModelIndex &) const override { return createIndex(row, column, row); } QModelIndex parent(const QModelIndex &) const override { return QModelIndex(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (parent == QModelIndex()) - return OpenFileInfoManager::instance().filteredItems(OpenFileInfo::RecentlyUsed).count(); + return OpenFileInfoManager::instance().filteredItems(OpenFileInfo::StatusFlag::RecentlyUsed).count(); else return 0; } }; class MDIWidget::MDIWidgetPrivate { private: QTreeView *listLRU; LRUItemModel *modelLRU; QVector<QWidget *> welcomeWidgets; static const QString configGroupName; static const QString configHeaderState; void createWelcomeWidget() { welcomeWidget = new QWidget(p); QGridLayout *layout = new QGridLayout(welcomeWidget); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); layout->setRowStretch(2, 0); layout->setRowStretch(3, 0); layout->setRowStretch(4, 10); layout->setRowStretch(5, 1); layout->setColumnStretch(0, 1); layout->setColumnStretch(1, 10); layout->setColumnStretch(2, 1); layout->setColumnStretch(3, 0); layout->setColumnStretch(4, 1); layout->setColumnStretch(5, 10); layout->setColumnStretch(6, 1); QLabel *label = new QLabel(i18n("<qt>Welcome to <b>KBibTeX</b></qt>"), welcomeWidget); layout->addWidget(label, 1, 2, 1, 3, Qt::AlignHCenter | Qt::AlignTop); QPushButton *buttonNew = new QPushButton(QIcon::fromTheme(QStringLiteral("document-new")), i18n("New"), welcomeWidget); layout->addWidget(buttonNew, 2, 2, 1, 1, Qt::AlignLeft | Qt::AlignBottom); connect(buttonNew, &QPushButton::clicked, p, &MDIWidget::documentNew); QPushButton *buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open..."), welcomeWidget); layout->addWidget(buttonOpen, 2, 4, 1, 1, Qt::AlignRight | Qt::AlignBottom); connect(buttonOpen, &QPushButton::clicked, p, &MDIWidget::documentOpen); label = new QLabel(i18n("List of recently used files:"), welcomeWidget); layout->addWidget(label, 3, 1, 1, 5, Qt::AlignLeft | Qt::AlignBottom); listLRU = new QTreeView(p); listLRU->setRootIsDecorated(false); listLRU->setSortingEnabled(true); listLRU->header()->setSectionResizeMode(QHeaderView::ResizeToContents); layout->addWidget(listLRU, 4, 1, 1, 5); connect(listLRU, &QTreeView::activated, p, &MDIWidget::slotOpenLRU); label->setBuddy(listLRU); p->addWidget(welcomeWidget); } void saveColumnsState() { QByteArray headerState = listLRU->header()->saveState(); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(configHeaderState, headerState); config->sync(); } void restoreColumnsState() { KConfigGroup configGroup(config, configGroupName); QByteArray headerState = configGroup.readEntry(configHeaderState, QByteArray()); if (!headerState.isEmpty()) listLRU->header()->restoreState(headerState); } public: MDIWidget *p; OpenFileInfo *currentFile; QWidget *welcomeWidget; KSharedConfigPtr config; MDIWidgetPrivate(MDIWidget *parent) : p(parent), currentFile(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))) { createWelcomeWidget(); modelLRU = new LRUItemModel(listLRU); QSortFilterProxyModel *sfpm = new QSortFilterProxyModel(listLRU); sfpm->setSourceModel(modelLRU); sfpm->setSortRole(LRUItemModel::SortRole); listLRU->setModel(sfpm); restoreColumnsState(); } ~MDIWidgetPrivate() { saveColumnsState(); delete welcomeWidget; } void addToMapper(OpenFileInfo *openFileInfo) { KParts::ReadOnlyPart *part = openFileInfo->part(p); connect(part, static_cast<void(KParts::ReadOnlyPart::*)()>(&KParts::ReadOnlyPart::completed), p, [this, openFileInfo]() { loadingFinished(openFileInfo); }); } void updateLRU() { modelLRU->reset(); } void loadingFinished(OpenFileInfo *ofi) { const QUrl oldUrl = ofi->url(); const QUrl newUrl = ofi->part(p)->url(); if (oldUrl != newUrl) { qCDebug(LOG_KBIBTEX_PROGRAM) << "Url changed from " << oldUrl.url(QUrl::PreferLocalFile) << " to " << newUrl.url(QUrl::PreferLocalFile); OpenFileInfoManager::instance().changeUrl(ofi, newUrl); /// completely opened or saved files should be marked as "recently used" - ofi->addFlags(OpenFileInfo::RecentlyUsed); + ofi->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "setCaption", Qt::DirectConnection, QGenericReturnArgument(), Q_ARG(QString, QString(QStringLiteral("%1 [%2]")).arg(ofi->shortCaption(), squeeze_text(ofi->fullCaption(), 64)))); } } }; const QString MDIWidget::MDIWidgetPrivate::configGroupName = QStringLiteral("WelcomeWidget"); const QString MDIWidget::MDIWidgetPrivate::configHeaderState = QStringLiteral("LRUlistHeaderState"); MDIWidget::MDIWidget(QWidget *parent) : QStackedWidget(parent), d(new MDIWidgetPrivate(this)) { connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &MDIWidget::slotStatusFlagsChanged); } MDIWidget::~MDIWidget() { delete d; } /// Clazy warns: "Missing reference on non-trivial type" for argument 'servicePtr', /// but type 'KService::Ptr' is actually a pointer (QExplicitlySharedDataPointer). void MDIWidget::setFile(OpenFileInfo *openFileInfo, KService::Ptr servicePtr) { KParts::Part *part = openFileInfo == nullptr ? nullptr : openFileInfo->part(this, servicePtr); /// 'part' will only be NULL if no OpenFileInfo object was given, /// or if the OpenFileInfo could not locate a KPart QWidget *widget = d->welcomeWidget; ///< by default use Welcome widget if (part != nullptr) { /// KPart object was located, use its object (instead of Welcome widget) widget = part->widget(); } else if (openFileInfo != nullptr) { /// A valid OpenFileInfo was given, but no KPart could be located OpenFileInfoManager::instance().close(openFileInfo); // FIXME does not close correctly if file is new const QString filename = openFileInfo->url().fileName(); if (filename.isEmpty()) KMessageBox::error(this, i18n("No part available for file of mime type '%1'.", openFileInfo->mimeType()), i18n("No Part Available")); else KMessageBox::error(this, i18n("No part available for file '%1'.", filename), i18n("No Part Available")); return; } FileView *oldEditor = nullptr; bool hasChanged = true; if (indexOf(widget) >= 0) { /// Chosen widget is already known (Welcome widget or a previously used KPart) /// In case previous (still current) widget was a KBibTeX Part, remember its editor PartWidget *currentPartWidget = qobject_cast<PartWidget *>(currentWidget()); oldEditor = currentPartWidget == nullptr ? nullptr : currentPartWidget->fileView(); /// Record if set widget is different from previous (still current) widget hasChanged = widget != currentWidget(); } else if (openFileInfo != nullptr) { /// Widget was not known previously, but a valid (new?) OpenFileInfo was given addWidget(widget); d->addToMapper(openFileInfo); } setCurrentWidget(widget); d->currentFile = openFileInfo; if (hasChanged) { /// This signal gets forwarded to KParts::MainWindow::createGUI(KParts::Part*) emit activePartChanged(part); /// If new widget comes from a KBibTeX Part, retrieve its editor PartWidget *newPartWidget = qobject_cast<PartWidget *>(widget); FileView *newEditor = newPartWidget == nullptr ? nullptr : newPartWidget->fileView(); emit documentSwitched(oldEditor, newEditor); } /// Notify main window about a change of current file, /// so that the title may contain the current file's URL. /// This signal will be connected to KMainWindow::setCaption. if (openFileInfo != nullptr) { QUrl url = openFileInfo->url(); if (url.isValid()) emit setCaption(QString(QStringLiteral("%1 [%2]")).arg(openFileInfo->shortCaption(), squeeze_text(openFileInfo->fullCaption(), 64))); else emit setCaption(openFileInfo->shortCaption()); } else emit setCaption(QString()); } FileView *MDIWidget::fileView() { OpenFileInfo *ofi = OpenFileInfoManager::instance().currentFile(); return qobject_cast<PartWidget *>(ofi->part(this)->widget())->fileView(); } OpenFileInfo *MDIWidget::currentFile() { return d->currentFile; } void MDIWidget::slotStatusFlagsChanged(OpenFileInfo::StatusFlags statusFlags) { - if (statusFlags.testFlag(OpenFileInfo::RecentlyUsed)) + if (statusFlags.testFlag(OpenFileInfo::StatusFlag::RecentlyUsed)) d->updateLRU(); } void MDIWidget::slotOpenLRU(const QModelIndex &index) { QUrl url = index.data(LRUItemModel::URLRole).toUrl(); if (url.isValid()) emit documentOpenURL(url); } #include "mdiwidget.moc" diff --git a/src/program/openfileinfo.cpp b/src/program/openfileinfo.cpp index 3a3f50aa..af590b8f 100644 --- a/src/program/openfileinfo.cpp +++ b/src/program/openfileinfo.cpp @@ -1,713 +1,713 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "openfileinfo.h" #include <QString> #include <QTimer> #include <QFileInfo> #include <QWidget> #include <QUrl> #include <QApplication> #include <KLocalizedString> #include <KConfig> #include <KConfigGroup> #include <KSharedConfig> #include <KMimeTypeTrader> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <KParts/ReadWritePart> #include <FileImporterPDF> #include "logging_program.h" class OpenFileInfo::OpenFileInfoPrivate { private: static int globalCounter; int m_counter; public: static const QString keyLastAccess; static const QString keyURL; static const QString dateTimeFormat; OpenFileInfo *p; KParts::ReadOnlyPart *part; KService::Ptr internalServicePtr; QWidget *internalWidgetParent; QDateTime lastAccessDateTime; StatusFlags flags; OpenFileInfoManager *openFileInfoManager; QString mimeType; QUrl url; OpenFileInfoPrivate(OpenFileInfoManager *openFileInfoManager, const QUrl &url, const QString &mimeType, OpenFileInfo *p) : m_counter(-1), p(p), part(nullptr), internalServicePtr(KService::Ptr()), internalWidgetParent(nullptr), flags(nullptr) { this->openFileInfoManager = openFileInfoManager; this->url = url; if (this->url.isValid() && this->url.scheme().isEmpty()) qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << this->url.toDisplayString(); this->mimeType = mimeType; } ~OpenFileInfoPrivate() { if (part != nullptr) { KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part); if (rwp != nullptr) rwp->closeUrl(true); delete part; } } KParts::ReadOnlyPart *createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr()) { - if (!p->flags().testFlag(OpenFileInfo::Open)) { + if (!p->flags().testFlag(OpenFileInfo::StatusFlag::Open)) { qCWarning(LOG_KBIBTEX_PROGRAM) << "Cannot create part for a file which is not open"; return nullptr; } Q_ASSERT_X(internalWidgetParent == nullptr || internalWidgetParent == newWidgetParent, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "internal widget should be either NULL or the same one as supplied as \"newWidgetParent\""); /** use cached part for this parent if possible */ if (internalWidgetParent == newWidgetParent && (newServicePtr == KService::Ptr() || internalServicePtr == newServicePtr)) { Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "Part is NULL"); return part; } else if (part != nullptr) { KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(part); if (rwp != nullptr) rwp->closeUrl(true); part->deleteLater(); part = nullptr; } /// reset to invalid values in case something goes wrong internalServicePtr = KService::Ptr(); internalWidgetParent = nullptr; if (!newServicePtr) { /// no valid KService has been passed /// try to find a read-write part to open file newServicePtr = p->defaultService(); } if (!newServicePtr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS"); qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS"); qCCritical(LOG_KBIBTEX_PROGRAM) << "Cannot find service to handle mimetype " << mimeType << endl; return nullptr; } QString errorString; part = newServicePtr->createInstance<KParts::ReadWritePart>(newWidgetParent, (QObject *)newWidgetParent, QVariantList(), &errorString); if (part == nullptr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "PATH=" << getenv("PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "LD_LIBRARY_PATH=" << getenv("LD_LIBRARY_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "XDG_DATA_DIRS=" << getenv("XDG_DATA_DIRS"); qCDebug(LOG_KBIBTEX_PROGRAM) << "QT_PLUGIN_PATH=" << getenv("QT_PLUGIN_PATH"); qCDebug(LOG_KBIBTEX_PROGRAM) << "KDEDIRS=" << getenv("KDEDIRS"); qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not instantiate read-write part for service" << newServicePtr->name() << "(mimeType=" << mimeType << ", library=" << newServicePtr->library() << ", error msg=" << errorString << ")"; /// creating a read-write part failed, so maybe it is read-only (like Okular's PDF viewer)? part = newServicePtr->createInstance<KParts::ReadOnlyPart>(newWidgetParent, (QObject *)newWidgetParent, QVariantList(), &errorString); } if (part == nullptr) { /// still cannot create part, must be error qCCritical(LOG_KBIBTEX_PROGRAM) << "Could not instantiate part for service" << newServicePtr->name() << "(mimeType=" << mimeType << ", library=" << newServicePtr->library() << ", error msg=" << errorString << ")"; return nullptr; } if (url.isValid()) { /// open URL in part part->openUrl(url); /// update document list widget accordingly - p->addFlags(OpenFileInfo::RecentlyUsed); - p->addFlags(OpenFileInfo::HasName); + p->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed); + p->addFlags(OpenFileInfo::StatusFlag::HasName); } else { /// initialize part with empty document part->openUrl(QUrl()); } - p->addFlags(OpenFileInfo::Open); + p->addFlags(OpenFileInfo::StatusFlag::Open); internalServicePtr = newServicePtr; internalWidgetParent = newWidgetParent; Q_ASSERT_X(part != nullptr, "KParts::ReadOnlyPart *OpenFileInfo::OpenFileInfoPrivate::createPart(QWidget *newWidgetParent, KService::Ptr newServicePtr = KService::Ptr())", "Creation of part failed, is NULL"); /// test should not be necessary, but just to be save ... return part; } int counter() { if (!url.isValid() && m_counter < 0) m_counter = ++globalCounter; else if (url.isValid()) qCWarning(LOG_KBIBTEX_PROGRAM) << "This function should not be called if URL is valid"; return m_counter; } }; int OpenFileInfo::OpenFileInfoPrivate::globalCounter = 0; const QString OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat = QStringLiteral("yyyy-MM-dd-hh-mm-ss-zzz"); const QString OpenFileInfo::OpenFileInfoPrivate::keyLastAccess = QStringLiteral("LastAccess"); const QString OpenFileInfo::OpenFileInfoPrivate::keyURL = QStringLiteral("URL"); OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QUrl &url) : d(new OpenFileInfoPrivate(openFileInfoManager, url, FileInfo::mimeTypeForUrl(url).name(), this)) { /// nothing } OpenFileInfo::OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QString &mimeType) : d(new OpenFileInfoPrivate(openFileInfoManager, QUrl(), mimeType, this)) { /// nothing } OpenFileInfo::~OpenFileInfo() { delete d; } void OpenFileInfo::setUrl(const QUrl &url) { Q_ASSERT_X(url.isValid(), "void OpenFileInfo::setUrl(const QUrl&)", "URL is not valid"); d->url = url; if (d->url.scheme().isEmpty()) qCWarning(LOG_KBIBTEX_PROGRAM) << "No scheme specified for URL" << d->url.toDisplayString(); d->mimeType = FileInfo::mimeTypeForUrl(url).name(); - addFlags(OpenFileInfo::HasName); + addFlags(OpenFileInfo::StatusFlag::HasName); } QUrl OpenFileInfo::url() const { return d->url; } bool OpenFileInfo::isModified() const { KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part); if (rwPart == nullptr) return false; else return rwPart->isModified(); } bool OpenFileInfo::save() { KParts::ReadWritePart *rwPart = qobject_cast< KParts::ReadWritePart *>(d->part); if (rwPart == nullptr) return true; else return rwPart->save(); } bool OpenFileInfo::close() { if (d->part == nullptr) { /// if there is no part, closing always "succeeds" return true; } KParts::ReadWritePart *rwp = qobject_cast<KParts::ReadWritePart *>(d->part); if (rwp == nullptr || rwp->closeUrl(true)) { d->part->deleteLater(); d->part = nullptr; d->internalWidgetParent = nullptr; return true; } return false; } QString OpenFileInfo::mimeType() const { return d->mimeType; } QString OpenFileInfo::shortCaption() const { if (d->url.isValid()) return d->url.fileName(); else return i18n("Unnamed-%1", d->counter()); } QString OpenFileInfo::fullCaption() const { if (d->url.isValid()) return d->url.url(QUrl::PreferLocalFile); else return shortCaption(); } /// Clazy warns: "Missing reference on non-trivial type" for argument 'servicePtr', /// but type 'KService::Ptr' is actually a pointer (QExplicitlySharedDataPointer). KParts::ReadOnlyPart *OpenFileInfo::part(QWidget *parent, KService::Ptr servicePtr) { return d->createPart(parent, servicePtr); } OpenFileInfo::StatusFlags OpenFileInfo::flags() const { return d->flags; } void OpenFileInfo::setFlags(StatusFlags statusFlags) { /// disallow files without name or valid url to become favorites - if (!d->url.isValid() || !d->flags.testFlag(HasName)) statusFlags &= ~Favorite; + if (!d->url.isValid() || !d->flags.testFlag(StatusFlag::HasName)) statusFlags &= ~static_cast<int>(StatusFlag::Favorite); /// files that got opened are by definition recently used files - if (!d->url.isValid() && d->flags.testFlag(Open)) statusFlags &= RecentlyUsed; + if (!d->url.isValid() && d->flags.testFlag(StatusFlag::Open)) statusFlags &= StatusFlag::RecentlyUsed; bool hasChanged = d->flags != statusFlags; d->flags = statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } void OpenFileInfo::addFlags(StatusFlags statusFlags) { /// disallow files without name or valid url to become favorites - if (!d->url.isValid() || !d->flags.testFlag(HasName)) statusFlags &= ~Favorite; + if (!d->url.isValid() || !d->flags.testFlag(StatusFlag::HasName)) statusFlags &= ~static_cast<int>(StatusFlag::Favorite); bool hasChanged = (~d->flags & statusFlags) > 0; d->flags |= statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } void OpenFileInfo::removeFlags(StatusFlags statusFlags) { bool hasChanged = (d->flags & statusFlags) > 0; d->flags &= ~statusFlags; if (hasChanged) emit flagsChanged(statusFlags); } QDateTime OpenFileInfo::lastAccess() const { return d->lastAccessDateTime; } void OpenFileInfo::setLastAccess(const QDateTime &dateTime) { d->lastAccessDateTime = dateTime; - emit flagsChanged(OpenFileInfo::RecentlyUsed); + emit flagsChanged(OpenFileInfo::StatusFlag::RecentlyUsed); } KService::List OpenFileInfo::listOfServices() { const QString mt = mimeType(); /// First, try to locate KPart that can both read and write the queried MIME type KService::List result = KMimeTypeTrader::self()->query(mt, QStringLiteral("KParts/ReadWritePart")); if (result.isEmpty()) { /// Second, if no 'writing' KPart was found, try to locate KPart that can at least read the queried MIME type result = KMimeTypeTrader::self()->query(mt, QStringLiteral("KParts/ReadOnlyPart")); if (result.isEmpty()) { /// If not even a 'reading' KPart was found, something is off, so warn the user and stop here qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not find any KPart that reads or writes mimetype" << mt; return result; } } /// Always include KBibTeX's KPart in list of services: /// First, check if KBibTeX's KPart is already in list as returned by /// KMimeTypeTrader::self()->query(..) bool listIncludesKBibTeXPart = false; for (KService::List::ConstIterator it = result.constBegin(); it != result.constEnd(); ++it) { qCDebug(LOG_KBIBTEX_PROGRAM) << "Found library for" << mt << ":" << (*it)->library(); listIncludesKBibTeXPart |= (*it)->library() == QStringLiteral("kbibtexpart"); } /// Then, if KBibTeX's KPart is not in the list, try to located it by desktop name if (!listIncludesKBibTeXPart) { KService::Ptr kbibtexpartByDesktopName = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); if (kbibtexpartByDesktopName != nullptr) { result << kbibtexpartByDesktopName; qCDebug(LOG_KBIBTEX_PROGRAM) << "Adding library for" << mt << ":" << kbibtexpartByDesktopName->library(); } else { qCDebug(LOG_KBIBTEX_PROGRAM) << "Could not locate KBibTeX's KPart neither by MIME type search, nor by desktop name"; } } return result; } KService::Ptr OpenFileInfo::defaultService() { const QString mt = mimeType(); KService::Ptr result; if (mt == QStringLiteral("application/pdf") || mt == QStringLiteral("text/x-bibtex")) { /// If either a BibTeX file or a PDF file is to be opened, enforce using /// KBibTeX's part over anything else. /// KBibTeX has a FileImporterPDF which allows it to load .pdf file /// that got generated with KBibTeX and contain the original /// .bib file as an 'attachment'. /// This importer does not work with any other .pdf files!!! result = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); } if (result == nullptr) { /// First, try to locate KPart that can both read and write the queried MIME type result = KMimeTypeTrader::self()->preferredService(mt, QStringLiteral("KParts/ReadWritePart")); if (result == nullptr) { /// Second, if no 'writing' KPart was found, try to locate KPart that can at least read the queried MIME type result = KMimeTypeTrader::self()->preferredService(mt, QStringLiteral("KParts/ReadOnlyPart")); if (result == nullptr && mt == QStringLiteral("text/x-bibtex")) /// Third, if MIME type is for BibTeX files, try loading KBibTeX part via desktop name result = KService::serviceByDesktopName(QStringLiteral("kbibtexpart")); } } if (result != nullptr) qCDebug(LOG_KBIBTEX_PROGRAM) << "Using service" << result->name() << "(" << result->comment() << ") for mime type" << mt << "through library" << result->library(); else qCWarning(LOG_KBIBTEX_PROGRAM) << "Could not find service for mime type" << mt; return result; } KService::Ptr OpenFileInfo::currentService() { return d->internalServicePtr; } class OpenFileInfoManager::OpenFileInfoManagerPrivate { private: static const QString configGroupNameRecentlyUsed; static const QString configGroupNameFavorites; static const QString configGroupNameOpen; static const int maxNumRecentlyUsedFiles, maxNumFavoriteFiles, maxNumOpenFiles; public: OpenFileInfoManager *p; OpenFileInfoManager::OpenFileInfoList openFileInfoList; OpenFileInfo *currentFileInfo; OpenFileInfoManagerPrivate(OpenFileInfoManager *parent) : p(parent), currentFileInfo(nullptr) { /// nothing } ~OpenFileInfoManagerPrivate() { for (OpenFileInfoManager::OpenFileInfoList::Iterator it = openFileInfoList.begin(); it != openFileInfoList.end();) { OpenFileInfo *ofi = *it; delete ofi; it = openFileInfoList.erase(it); } } static bool byNameLessThan(const OpenFileInfo *left, const OpenFileInfo *right) { return left->shortCaption() < right->shortCaption(); } static bool byLRULessThan(const OpenFileInfo *left, const OpenFileInfo *right) { return left->lastAccess() > right->lastAccess(); /// reverse sorting! } void readConfig() { - readConfig(OpenFileInfo::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); - readConfig(OpenFileInfo::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); - readConfig(OpenFileInfo::Open, configGroupNameOpen, maxNumOpenFiles); + readConfig(OpenFileInfo::StatusFlag::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); + readConfig(OpenFileInfo::StatusFlag::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); + readConfig(OpenFileInfo::StatusFlag::Open, configGroupNameOpen, maxNumOpenFiles); } void writeConfig() { - writeConfig(OpenFileInfo::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); - writeConfig(OpenFileInfo::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); - writeConfig(OpenFileInfo::Open, configGroupNameOpen, maxNumOpenFiles); + writeConfig(OpenFileInfo::StatusFlag::RecentlyUsed, configGroupNameRecentlyUsed, maxNumRecentlyUsedFiles); + writeConfig(OpenFileInfo::StatusFlag::Favorite, configGroupNameFavorites, maxNumFavoriteFiles); + writeConfig(OpenFileInfo::StatusFlag::Open, configGroupNameOpen, maxNumOpenFiles); } void readConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); bool isFirst = true; KConfigGroup cg(config, configGroupName); for (int i = 0; i < maxNumFiles; ++i) { QUrl fileUrl = QUrl(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), "")); if (!fileUrl.isValid()) break; if (fileUrl.scheme().isEmpty()) fileUrl.setScheme(QStringLiteral("file")); /// For local files, test if they exist; ignore local files that do not exist if (fileUrl.isLocalFile()) { if (!QFileInfo::exists(fileUrl.toLocalFile())) continue; } OpenFileInfo *ofi = p->contains(fileUrl); if (ofi == nullptr) { ofi = p->open(fileUrl); } ofi->addFlags(statusFlag); - ofi->addFlags(OpenFileInfo::HasName); + ofi->addFlags(OpenFileInfo::StatusFlag::HasName); ofi->setLastAccess(QDateTime::fromString(cg.readEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ""), OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat)); if (isFirst) { isFirst = false; - if (statusFlag == OpenFileInfo::Open) + if (statusFlag == OpenFileInfo::StatusFlag::Open) p->setCurrentFile(ofi); } } } void writeConfig(OpenFileInfo::StatusFlag statusFlag, const QString &configGroupName, int maxNumFiles) { KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); KConfigGroup cg(config, configGroupName); OpenFileInfoManager::OpenFileInfoList list = p->filteredItems(statusFlag); int i = 0; for (OpenFileInfoManager::OpenFileInfoList::ConstIterator it = list.constBegin(); i < maxNumFiles && it != list.constEnd(); ++it, ++i) { OpenFileInfo *ofi = *it; cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i), ofi->url().url(QUrl::PreferLocalFile)); cg.writeEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i), ofi->lastAccess().toString(OpenFileInfo::OpenFileInfoPrivate::dateTimeFormat)); } for (; i < maxNumFiles; ++i) { cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyURL).arg(i)); cg.deleteEntry(QString(QStringLiteral("%1-%2")).arg(OpenFileInfo::OpenFileInfoPrivate::keyLastAccess).arg(i)); } config->sync(); } }; const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameRecentlyUsed = QStringLiteral("DocumentList-RecentlyUsed"); const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameFavorites = QStringLiteral("DocumentList-Favorites"); const QString OpenFileInfoManager::OpenFileInfoManagerPrivate::configGroupNameOpen = QStringLiteral("DocumentList-Open"); const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumFavoriteFiles = 256; const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumRecentlyUsedFiles = 8; const int OpenFileInfoManager::OpenFileInfoManagerPrivate::maxNumOpenFiles = 16; OpenFileInfoManager::OpenFileInfoManager(QObject *parent) : QObject(parent), d(new OpenFileInfoManagerPrivate(this)) { QTimer::singleShot(300, this, [this]() { d->readConfig(); }); } OpenFileInfoManager &OpenFileInfoManager::instance() { /// Allocate this singleton on heap not stack like most other singletons /// Supposedly, QCoreApplication will clean this singleton at application's end static OpenFileInfoManager *singleton = new OpenFileInfoManager(QCoreApplication::instance()); return *singleton; } OpenFileInfoManager::~OpenFileInfoManager() { delete d; } OpenFileInfo *OpenFileInfoManager::createNew(const QString &mimeType) { OpenFileInfo *result = new OpenFileInfo(this, mimeType); connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged); d->openFileInfoList << result; result->setLastAccess(); return result; } OpenFileInfo *OpenFileInfoManager::open(const QUrl &url) { Q_ASSERT_X(url.isValid(), "OpenFileInfo *OpenFileInfoManager::open(const QUrl&)", "URL is not valid"); OpenFileInfo *result = contains(url); if (result == nullptr) { /// file not yet open result = new OpenFileInfo(this, url); connect(result, &OpenFileInfo::flagsChanged, this, &OpenFileInfoManager::flagsChanged); d->openFileInfoList << result; } /// else: file was already open, re-use and return existing OpenFileInfo pointer result->setLastAccess(); return result; } OpenFileInfo *OpenFileInfoManager::contains(const QUrl &url) const { if (!url.isValid()) return nullptr; /// can only be unnamed file for (auto *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { if (ofi->url() == url) return ofi; } return nullptr; } bool OpenFileInfoManager::changeUrl(OpenFileInfo *openFileInfo, const QUrl &url) { OpenFileInfo *previouslyContained = contains(url); /// check if old url differs from new url and old url is valid - if (previouslyContained != nullptr && previouslyContained->flags().testFlag(OpenFileInfo::Open) && previouslyContained != openFileInfo) { + if (previouslyContained != nullptr && previouslyContained->flags().testFlag(OpenFileInfo::StatusFlag::Open) && previouslyContained != openFileInfo) { qCWarning(LOG_KBIBTEX_PROGRAM) << "Open file with same URL already exists, forcefully closing it" << endl; close(previouslyContained); } QUrl oldUrl = openFileInfo->url(); openFileInfo->setUrl(url); if (url != oldUrl && oldUrl.isValid()) { /// current document was most probabily renamed (e.g. due to "Save As") /// add old URL to recently used files, but exclude the open files list OpenFileInfo *ofi = open(oldUrl); // krazy:exclude=syscalls - OpenFileInfo::StatusFlags statusFlags = (openFileInfo->flags() & (~OpenFileInfo::Open)) | OpenFileInfo::RecentlyUsed; + OpenFileInfo::StatusFlags statusFlags = (openFileInfo->flags() & ~static_cast<int>(OpenFileInfo::StatusFlag::Open)) | OpenFileInfo::StatusFlag::RecentlyUsed; ofi->setFlags(statusFlags); } if (previouslyContained != nullptr) { /// keep Favorite flag if set in file that have previously same URL - if (previouslyContained->flags().testFlag(OpenFileInfo::Favorite)) - openFileInfo->setFlags(openFileInfo->flags() | OpenFileInfo::Favorite); + if (previouslyContained->flags().testFlag(OpenFileInfo::StatusFlag::Favorite)) + openFileInfo->setFlags(openFileInfo->flags() | OpenFileInfo::StatusFlag::Favorite); /// remove the old entry with the same url has it will be replaced by the new one d->openFileInfoList.remove(d->openFileInfoList.indexOf(previouslyContained)); previouslyContained->deleteLater(); - OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::Open; - statusFlags |= OpenFileInfo::RecentlyUsed; - statusFlags |= OpenFileInfo::Favorite; + OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::StatusFlag::Open; + statusFlags |= OpenFileInfo::StatusFlag::RecentlyUsed; + statusFlags |= OpenFileInfo::StatusFlag::Favorite; emit flagsChanged(statusFlags); } if (openFileInfo == d->currentFileInfo) emit currentChanged(openFileInfo, KService::Ptr()); emit flagsChanged(openFileInfo->flags()); return true; } bool OpenFileInfoManager::close(OpenFileInfo *openFileInfo) { if (openFileInfo == nullptr) { qCWarning(LOG_KBIBTEX_PROGRAM) << "void OpenFileInfoManager::close(OpenFileInfo *openFileInfo): Cannot close openFileInfo which is NULL"; return false; } bool isClosing = false; openFileInfo->setLastAccess(); /// remove flag "open" from file to be closed and determine which file to show instead OpenFileInfo *nextCurrent = (d->currentFileInfo == openFileInfo) ? nullptr : d->currentFileInfo; for (OpenFileInfo *ofi : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { if (!isClosing && ofi == openFileInfo && openFileInfo->close()) { isClosing = true; /// Mark file as closed (i.e. not open) - openFileInfo->removeFlags(OpenFileInfo::Open); + openFileInfo->removeFlags(OpenFileInfo::StatusFlag::Open); /// If file has a filename, remember as recently used - if (openFileInfo->flags().testFlag(OpenFileInfo::HasName)) - openFileInfo->addFlags(OpenFileInfo::RecentlyUsed); - } else if (nextCurrent == nullptr && ofi->flags().testFlag(OpenFileInfo::Open)) + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::HasName)) + openFileInfo->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed); + } else if (nextCurrent == nullptr && ofi->flags().testFlag(OpenFileInfo::StatusFlag::Open)) nextCurrent = ofi; } /// If the current document is to be closed, /// switch over to the next available one if (isClosing) setCurrentFile(nextCurrent); return isClosing; } bool OpenFileInfoManager::queryCloseAll() { /// Assume that all closing operations succeed bool isClosing = true; /// For keeping track of files that get closed here OpenFileInfoList restoreLaterList; /// For each file known ... for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(d->openFileInfoList)) { /// Check only open file (ignore recently used, favorites, ...) - if (openFileInfo->flags().testFlag(OpenFileInfo::Open)) { + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::Open)) { if (openFileInfo->close()) { /// If file could be closed without user canceling the operation ... /// Mark file as closed (i.e. not open) - openFileInfo->removeFlags(OpenFileInfo::Open); + openFileInfo->removeFlags(OpenFileInfo::StatusFlag::Open); /// If file has a filename, remember as recently used - if (openFileInfo->flags().testFlag(OpenFileInfo::HasName)) - openFileInfo->addFlags(OpenFileInfo::RecentlyUsed); + if (openFileInfo->flags().testFlag(OpenFileInfo::StatusFlag::HasName)) + openFileInfo->addFlags(OpenFileInfo::StatusFlag::RecentlyUsed); /// Remember file as to be marked as open later restoreLaterList.append(openFileInfo); } else { /// User chose to cancel closing operation, /// stop everything here isClosing = false; break; } } } if (isClosing) { /// Closing operation was not cancelled, therefore mark /// all files that were open before as open now. /// This makes the files to be reopened when KBibTeX is /// restarted again (assuming that this function was /// called when KBibTeX is exiting). for (OpenFileInfo *openFileInfo : const_cast<const OpenFileInfoManager::OpenFileInfoList &>(restoreLaterList)) { - openFileInfo->addFlags(OpenFileInfo::Open); + openFileInfo->addFlags(OpenFileInfo::StatusFlag::Open); } d->writeConfig(); } return isClosing; } OpenFileInfo *OpenFileInfoManager::currentFile() const { return d->currentFileInfo; } /// Clazy warns: "Missing reference on non-trivial type" for argument 'servicePtr', /// but type 'KService::Ptr' is actually a pointer (QExplicitlySharedDataPointer). void OpenFileInfoManager::setCurrentFile(OpenFileInfo *openFileInfo, KService::Ptr servicePtr) { bool hasChanged = d->currentFileInfo != openFileInfo; OpenFileInfo *previous = d->currentFileInfo; d->currentFileInfo = openFileInfo; if (d->currentFileInfo != nullptr) { - d->currentFileInfo->addFlags(OpenFileInfo::Open); + d->currentFileInfo->addFlags(OpenFileInfo::StatusFlag::Open); d->currentFileInfo->setLastAccess(); } if (hasChanged) { if (previous != nullptr) previous->setLastAccess(); emit currentChanged(openFileInfo, servicePtr); } else if (openFileInfo != nullptr && servicePtr != openFileInfo->currentService()) emit currentChanged(openFileInfo, servicePtr); } -OpenFileInfoManager::OpenFileInfoList OpenFileInfoManager::filteredItems(OpenFileInfo::StatusFlags required, OpenFileInfo::StatusFlags forbidden) +OpenFileInfoManager::OpenFileInfoList OpenFileInfoManager::filteredItems(OpenFileInfo::StatusFlag required, OpenFileInfo::StatusFlags forbidden) { OpenFileInfoList result; for (OpenFileInfoList::Iterator it = d->openFileInfoList.begin(); it != d->openFileInfoList.end(); ++it) { OpenFileInfo *ofi = *it; - if ((ofi->flags() & required) == required && (ofi->flags() & forbidden) == 0) + if (ofi->flags().testFlag(required) && (ofi->flags() & forbidden) == 0) result << ofi; } - if (required == OpenFileInfo::RecentlyUsed) + if (required == OpenFileInfo::StatusFlag::RecentlyUsed) std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byLRULessThan); - else if (required == OpenFileInfo::Favorite || required == OpenFileInfo::Open) + else if (required == OpenFileInfo::StatusFlag::Favorite || required == OpenFileInfo::StatusFlag::Open) std::sort(result.begin(), result.end(), OpenFileInfoManagerPrivate::byNameLessThan); return result; } void OpenFileInfoManager::deferredListsChanged() { - OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::Open; - statusFlags |= OpenFileInfo::RecentlyUsed; - statusFlags |= OpenFileInfo::Favorite; + OpenFileInfo::StatusFlags statusFlags = OpenFileInfo::StatusFlag::Open; + statusFlags |= OpenFileInfo::StatusFlag::RecentlyUsed; + statusFlags |= OpenFileInfo::StatusFlag::Favorite; emit flagsChanged(statusFlags); } diff --git a/src/program/openfileinfo.h b/src/program/openfileinfo.h index 2e63be30..3357eff9 100644 --- a/src/program/openfileinfo.h +++ b/src/program/openfileinfo.h @@ -1,167 +1,167 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_PROGRAM_OPENFILEINFO_H #define KBIBTEX_PROGRAM_OPENFILEINFO_H #include <QObject> #include <QList> #include <QDateTime> #include <QUrl> #include <QVariant> #include <KService> #include <FileInfo> namespace KParts { class ReadOnlyPart; } class OpenFileInfoManager; class OpenFileInfo : public QObject { Q_OBJECT public: - enum StatusFlag { + enum class StatusFlag { Open = 0x1, RecentlyUsed = 0x2, Favorite = 0x4, HasName = 0x8 }; Q_DECLARE_FLAGS(StatusFlags, StatusFlag) ~OpenFileInfo() override; KParts::ReadOnlyPart *part(QWidget *parent, KService::Ptr servicePtr = KService::Ptr()); QString shortCaption() const; QString fullCaption() const; QString mimeType() const; QUrl url() const; bool isModified() const; bool save(); /** * Close the current file. The user may interrupt the closing * if the file is modified and he/she presses "Cancel" when asked * to close the modified file. * * @return returns true if the closing was successful, otherwise false */ bool close(); StatusFlags flags() const; void setFlags(StatusFlags statusFlags); void addFlags(StatusFlags statusFlags); void removeFlags(StatusFlags statusFlags); QDateTime lastAccess() const; void setLastAccess(const QDateTime &dateTime = QDateTime::currentDateTime()); KService::List listOfServices(); KService::Ptr defaultService(); KService::Ptr currentService(); friend class OpenFileInfoManager; signals: void flagsChanged(OpenFileInfo::StatusFlags statusFlags); protected: OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QUrl &url); OpenFileInfo(OpenFileInfoManager *openFileInfoManager, const QString &mimeType = FileInfo::mimetypeBibTeX); void setUrl(const QUrl &url); private: class OpenFileInfoPrivate; OpenFileInfoPrivate *d; }; Q_DECLARE_METATYPE(OpenFileInfo *) class OpenFileInfoManager: public QObject { Q_OBJECT public: typedef QVector<OpenFileInfo *> OpenFileInfoList; static OpenFileInfoManager &instance(); ~OpenFileInfoManager() override; OpenFileInfo *createNew(const QString &mimeType = FileInfo::mimetypeBibTeX); /** * Open the given bibliography file as specified in the URL. * If the file is already open, an existing OpenFileInfo pointer will be * returned. If the file was not yet open, a new OpenFileInfo object will * be created and returned. * There shall be no two different OpenFileInfo objects representing * the same file. * @param url URL to bibliography file to open * @return an OpenFileInfo object representing the opened file */ OpenFileInfo *open(const QUrl &url); OpenFileInfo *contains(const QUrl &url) const; OpenFileInfo *currentFile() const; bool changeUrl(OpenFileInfo *openFileInfo, const QUrl &url); bool close(OpenFileInfo *openFileInfo); /** * Try to close all open files. If a file is modified, the user will be asked * if the file shall be saved first. Depending on the KPart, the user may opt * to cancel the closing operation for any modified file. * If the user chooses to cancel, this function quits the closing process and * returns false. Files closed so far stay closed, the remaining open files stay * open. * If the user does not interrupt this function (e.g. no file was modified or * the user confirmed to save or to discard changes), all files get closed. * However, all files that were open before will be marked as opened * again. This could render the user interface inconsistent (therefore this * function should be called only when exiting KBibTeX), but it allows to * easily reopen in the next session all files that were open at the time * this function was called in this session. * @return true if all files could be closed successfully, else false. */ bool queryCloseAll(); void setCurrentFile(OpenFileInfo *openFileInfo, KService::Ptr servicePtr = KService::Ptr()); - OpenFileInfoList filteredItems(OpenFileInfo::StatusFlags required, OpenFileInfo::StatusFlags forbidden = nullptr); + OpenFileInfoList filteredItems(OpenFileInfo::StatusFlag required, OpenFileInfo::StatusFlags forbidden = nullptr); friend class OpenFileInfo; signals: void currentChanged(OpenFileInfo *, KService::Ptr); void flagsChanged(OpenFileInfo::StatusFlags statusFlags); protected: explicit OpenFileInfoManager(QObject *parent); private: class OpenFileInfoManagerPrivate; OpenFileInfoManagerPrivate *d; private slots: void deferredListsChanged(); }; #endif // KBIBTEX_PROGRAM_OPENFILEINFO_H diff --git a/src/test/kbibtexfilestest.cpp b/src/test/kbibtexfilestest.cpp index 6e3a0fa2..ff05acc7 100644 --- a/src/test/kbibtexfilestest.cpp +++ b/src/test/kbibtexfilestest.cpp @@ -1,409 +1,409 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * Copyright (C) 2014 by Pino Toscano <pino@kde.org> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include <QtTest> #include <QCryptographicHash> #include <QTemporaryFile> #ifdef WRITE_RAWDATAFILE #include <QFile> #endif // WRITE_RAWDATAFILE #include <File> #include <Entry> #include <FileImporterBibTeX> #include <FileExporterBibTeX> /// Provides definition of TESTSET_DIRECTORY #include "test-config.h" #ifndef WRITE_RAWDATAFILE #include "kbibtexfilestest-rawdata.h" #endif // WRITE_RAWDATAFILE #include "logging_test.h" typedef struct { QString filename; #ifndef WRITE_RAWDATAFILE int numElements, numEntries; QString lastEntryId, lastEntryLastAuthorLastName; QByteArray hashLastAuthors, hashFilesUrlsDoi; #endif // WRITE_RAWDATAFILE } TestFile; Q_DECLARE_METATYPE(TestFile) class KBibTeXFilesTest : public QObject { Q_OBJECT private slots: void initTestCase(); #ifdef WRITE_RAWDATAFILE void cleanupTestCase(); #endif // WRITE_RAWDATAFILE void testFiles_data(); void testFiles(); private: /** * Load a bibliography file and checks a number of known properties * such as number of elements/entries or the hash sum of authors' last names. * It is the caller's responsibility to pass a valid argument to @p outFile * and later delete the returned File object. * * @param absoluteFilename biblography file to laod * @param currentTestFile data structure holding the baseline values * @param outFile returns pointer to the opened file */ void loadFile(const QString &absoluteFilename, const TestFile &currentTestFile, File **outFile); /** * Save a bibliography in a temporary file. * It is the caller's responsibility to pass a valid argument to @p outFile, * which will hold the temporary file's name upon successful return. * * @param file bibliography data structure to be saved * @param currentTestFile baseline data structure used to determine temporary file's name * @param outFile returns the temporary file's name */ void saveFile(File *file, const TestFile &currentTestFile, QString *outFile); /** * Create and fill a TestFile data structure based on the provided values. * * @param filename Bibliography file's filename * @param numElements Number of elements to expect in bibliography * @param numEntries Number of entries to expect in bibliography * @param lastEntryId Identifier of last entry in bibliography * @param lastEntryLastAuthorLastName Last author's last name in bibliography * @param hashLastAuthors The hash sum over all authors/editors in bibliography * @param hashFilesUrlsDoi The hash sum over all URLs and DOIs in bibliography * @return An initialized TestFile data structure */ TestFile createTestFile(const QString &filename #ifndef WRITE_RAWDATAFILE , int numElements, int numEntries, const QString &lastEntryId, const QString &lastEntryLastAuthorLastName, const QByteArray &hashLastAuthors, const QByteArray &hashFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); }; void KBibTeXFilesTest::initTestCase() { qRegisterMetaType<TestFile>("TestFile"); #ifdef WRITE_RAWDATAFILE QFile rawDataFile("kbibtexfilestest-rawdata.h"); if (rawDataFile.open(QFile::WriteOnly)) { QTextStream ts(&rawDataFile); ts << QStringLiteral("/********************************************************************************") << endl << endl; ts << QStringLiteral("Copyright ") << QDate::currentDate().year() << QStringLiteral(" Thomas Fischer <fischer@unix-ag.uni-kl.de> and others") << endl << endl; ts << QStringLiteral("Redistribution and use in source and binary forms, with or without") << endl << QStringLiteral("modification, are permitted provided that the following conditions") << endl << QStringLiteral("are met:") << endl << endl; ts << QStringLiteral("1. Redistributions of source code must retain the above copyright") << endl << QStringLiteral(" notice, this list of conditions and the following disclaimer.") << endl; ts << QStringLiteral("2. Redistributions in binary form must reproduce the above copyright") << endl << QStringLiteral(" notice, this list of conditions and the following disclaimer in the") << endl << QStringLiteral(" documentation and/or other materials provided with the distribution.") << endl << endl; ts << QStringLiteral("THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR") << endl << QStringLiteral("IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES") << endl << QStringLiteral("OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.") << endl << QStringLiteral("IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,") << endl << QStringLiteral("INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT") << endl << QStringLiteral("NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,") << endl << QStringLiteral("DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY") << endl << QStringLiteral("THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT") << endl << QStringLiteral("(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF") << endl << QStringLiteral("THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.") << endl << endl; ts << QStringLiteral("********************************************************************************/") << endl << endl; ts << QStringLiteral("#ifndef KBIBTEX_FILES_TEST_RAWDATA_H") << endl << QStringLiteral("#define KBIBTEX_FILES_TEST_RAWDATA_H") << endl << endl; rawDataFile.close(); } #endif // WRITE_RAWDATAFILE } #ifdef WRITE_RAWDATAFILE void KBibTeXFilesTest::cleanupTestCase() { QFile rawDataFile("kbibtexfilestest-rawdata.h"); if (rawDataFile.open(QFile::Append)) { QTextStream ts(&rawDataFile); ts << endl << QStringLiteral("#endif // KBIBTEX_FILES_TEST_RAWDATA_H") << endl; rawDataFile.close(); } } #endif // WRITE_RAWDATAFILE void KBibTeXFilesTest::testFiles_data() { QTest::addColumn<TestFile>("testFile"); QTest::newRow("bug19489.bib") << createTestFile(QStringLiteral("bib/bug19489.bib") #ifndef WRITE_RAWDATAFILE , bug19489NumElements, bug19489NumEntries, bug19489LastEntryId, bug19489LastAuthor, bug19489LastAuthors, bug19489FilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("names-with-braces.bib") << createTestFile(QStringLiteral("bib/names-with-braces.bib") #ifndef WRITE_RAWDATAFILE , nameswithbracesNumElements, nameswithbracesNumEntries, nameswithbracesLastEntryId, nameswithbracesLastAuthor, nameswithbracesLastAuthors, nameswithbracesFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("duplicates.bib") << createTestFile(QStringLiteral("bib/duplicates.bib") #ifndef WRITE_RAWDATAFILE , duplicatesNumElements, duplicatesNumEntries, duplicatesLastEntryId, duplicatesLastAuthor, duplicatesLastAuthors, duplicatesFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("minix.bib") << createTestFile(QStringLiteral("bib/minix.bib") #ifndef WRITE_RAWDATAFILE , minixNumElements, minixNumEntries, minixLastEntryId, minixLastAuthor, minixLastAuthors, minixFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("bug19484-refs.bib") << createTestFile(QStringLiteral("bib/bug19484-refs.bib") #ifndef WRITE_RAWDATAFILE , bug19484refsNumElements, bug19484refsNumEntries, bug19484refsLastEntryId, bug19484refsLastAuthor, bug19484refsLastAuthors, bug19484refsFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("bug19362-file15701-database.bib") << createTestFile(QStringLiteral("bib/bug19362-file15701-database.bib") #ifndef WRITE_RAWDATAFILE , bug19362file15701databaseNumElements, bug19362file15701databaseNumEntries, bug19362file15701databaseLastEntryId, bug19362file15701databaseLastAuthor, bug19362file15701databaseLastAuthors, bug19362file15701databaseFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("digiplay.bib") << createTestFile(QStringLiteral("bib/digiplay.bib") #ifndef WRITE_RAWDATAFILE , digiplayNumElements, digiplayNumEntries, digiplayLastEntryId, digiplayLastAuthor, digiplayLastAuthors, digiplayFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("backslash.bib") << createTestFile(QStringLiteral("bib/backslash.bib") #ifndef WRITE_RAWDATAFILE , backslashNumElements, backslashNumEntries, backslashLastEntryId, backslashLastAuthor, backslashLastAuthors, backslashFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("bug379443-attachment105313-IOPEXPORT_BIB.bib") << createTestFile(QStringLiteral("bib/bug379443-attachment105313-IOPEXPORT_BIB.bib") #ifndef WRITE_RAWDATAFILE , bug379443attachment105313IOPEXPORTBIBNumElements, bug379443attachment105313IOPEXPORTBIBNumEntries, bug379443attachment105313IOPEXPORTBIBLastEntryId, bug379443attachment105313IOPEXPORTBIBLastAuthor, bug379443attachment105313IOPEXPORTBIBLastAuthors, bug379443attachment105313IOPEXPORTBIBFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("bug21870-polito.bib") << createTestFile(QStringLiteral("bib/bug21870-polito.bib") #ifndef WRITE_RAWDATAFILE , bug21870politoNumElements, bug21870politoNumEntries, bug21870politoLastEntryId, bug21870politoLastAuthor, bug21870politoLastAuthors, bug21870politoFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); QTest::newRow("cloud-duplicates.bib") << createTestFile(QStringLiteral("bib/cloud-duplicates.bib") #ifndef WRITE_RAWDATAFILE , cloudduplicatesNumElements, cloudduplicatesNumEntries, cloudduplicatesLastEntryId, cloudduplicatesLastAuthor, cloudduplicatesLastAuthors, cloudduplicatesFilesUrlsDoi #endif // WRITE_RAWDATAFILE ); } void KBibTeXFilesTest::testFiles() { QFETCH(TestFile, testFile); const QString absoluteFilename = QLatin1String(TESTSET_DIRECTORY "/") + testFile.filename; QVERIFY(QFileInfo::exists(absoluteFilename)); /// First load the file ... File *file = nullptr; loadFile(absoluteFilename, testFile, &file); QVERIFY(file); #ifndef WRITE_RAWDATAFILE /// ... then save it again to file ... QString tempFileName; saveFile(file, testFile, &tempFileName); QVERIFY(!tempFileName.isEmpty()); /// ... and finally try to load again the newly saved version File *file2 = nullptr; loadFile(tempFileName, testFile, &file2); QVERIFY(file2); QFile::remove(tempFileName); #endif // WRITE_RAWDATAFILE delete file; #ifndef WRITE_RAWDATAFILE delete file2; #endif // WRITE_RAWDATAFILE } void KBibTeXFilesTest::loadFile(const QString &absoluteFilename, const TestFile &currentTestFile, File **outFile) { *outFile = nullptr; FileImporterBibTeX *importer = nullptr; if (currentTestFile.filename.endsWith(QStringLiteral(".bib"))) { importer = new FileImporterBibTeX(this); - importer->setCommentHandling(FileImporterBibTeX::KeepComments); + importer->setCommentHandling(FileImporterBibTeX::CommentHandling::Keep); } else { QFAIL(qPrintable(QString::fromLatin1("Don't know format of '%1'").arg(currentTestFile.filename))); } QFile file(absoluteFilename); if (file.open(QFile::ReadOnly)) { const QByteArray fileData = file.readAll(); file.close(); const QByteArray hashData = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); qCInfo(LOG_KBIBTEX_TEST) << "MD5 for file" << absoluteFilename << "is" << hashData.toHex(); } File *bibTeXFile = nullptr; QVERIFY(file.open(QFile::ReadOnly)); bibTeXFile = importer->load(&file); file.close(); qCInfo(LOG_KBIBTEX_TEST) << (bibTeXFile == nullptr ? "bibTeXFile is NULL" : (bibTeXFile->isEmpty() ? "bibTeXFile is EMPTY" : QString(QStringLiteral("bibTeXFile contains %1 elements")).arg(bibTeXFile->count()).toLatin1())); QVERIFY(bibTeXFile); QVERIFY(!bibTeXFile->isEmpty()); QStringList lastAuthorsList, filesUrlsDoiList; lastAuthorsList.reserve(bibTeXFile->size()); const int numElements = bibTeXFile->count(); int numEntries = 0; QString lastEntryId, lastEntryLastAuthorLastName; for (const auto &element : const_cast<const File &>(*bibTeXFile)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { ++numEntries; lastEntryId = entry->id(); Value authors = entry->value(Entry::ftAuthor); if (!authors.isEmpty()) { ValueItem *vi = authors.last().data(); Person *p = dynamic_cast<Person *>(vi); if (p != nullptr) { lastEntryLastAuthorLastName = p->lastName(); } else lastEntryLastAuthorLastName.clear(); } else { Value editors = entry->value(Entry::ftEditor); if (!editors.isEmpty()) { ValueItem *vi = editors.last().data(); Person *p = dynamic_cast<Person *>(vi); if (p != nullptr) { lastEntryLastAuthorLastName = p->lastName(); } else lastEntryLastAuthorLastName.clear(); } else lastEntryLastAuthorLastName.clear(); } if (!lastEntryLastAuthorLastName.isEmpty()) { if (lastEntryLastAuthorLastName[0] == QLatin1Char('{') && lastEntryLastAuthorLastName[lastEntryLastAuthorLastName.length() - 1] == QLatin1Char('}')) lastEntryLastAuthorLastName = lastEntryLastAuthorLastName.mid(1, lastEntryLastAuthorLastName.length() - 2); lastAuthorsList << lastEntryLastAuthorLastName; } static const QStringList stems {Entry::ftUrl, Entry::ftDOI, Entry::ftLocalFile, Entry::ftFile}; for (const QString &stem : stems) { for (int index = 1; index < 100; ++index) { const QString field = index == 1 ? stem : QString(QStringLiteral("%1%2")).arg(stem).arg(index); const Value v = entry->value(field); for (const QSharedPointer<ValueItem> &vi : v) { filesUrlsDoiList << PlainTextValue::text(vi); } if (v.isEmpty() && index > 10) break; } } } } #ifdef WRITE_RAWDATAFILE static const QRegularExpression filenameStemRegExp(QStringLiteral("/?([^/]+)[.]bib$")); const QString filenameStem = filenameStemRegExp.match(currentTestFile.filename).captured(1).remove(QChar('-')).remove(QChar('_')); QFile rawDataFile("kbibtexfilestest-rawdata.h"); if (rawDataFile.open(QFile::Append)) { QTextStream ts(&rawDataFile); ts << QStringLiteral("static const int ") << filenameStem << QStringLiteral("NumElements = ") << QString::number(numElements) << QStringLiteral(";\n"); ts << QStringLiteral("static const int ") << filenameStem << QStringLiteral("NumEntries = ") << QString::number(numEntries) << QStringLiteral(";\n"); ts << QStringLiteral("static const QString ") << filenameStem << QStringLiteral("LastEntryId = QStringLiteral(\"") << lastEntryId << QStringLiteral("\");\n"); ts << QStringLiteral("static const QString ") << filenameStem << QStringLiteral("LastAuthor = QStringLiteral(\"") << lastEntryLastAuthorLastName << QStringLiteral("\");\n"); rawDataFile.close(); } #else // WRITE_RAWDATAFILE QCOMPARE(currentTestFile.numElements, numElements); QCOMPARE(currentTestFile.numEntries, numEntries); QCOMPARE(currentTestFile.lastEntryId, lastEntryId); QCOMPARE(currentTestFile.lastEntryLastAuthorLastName, lastEntryLastAuthorLastName); #endif // WRITE_RAWDATAFILE QCryptographicHash hashLastAuthors(QCryptographicHash::Md5); for (const QString &lastAuthor : const_cast<const QStringList &>(lastAuthorsList)) { const QByteArray lastAuthorUtf8 = lastAuthor.toUtf8(); hashLastAuthors.addData(lastAuthorUtf8); } #ifdef WRITE_RAWDATAFILE if (rawDataFile.open(QFile::Append)) { QTextStream ts(&rawDataFile); ts << QStringLiteral("static const QByteArray ") << filenameStem << QStringLiteral("LastAuthors = QByteArray::fromHex(\"") << hashLastAuthors.result().toHex() << QStringLiteral("\");\n"); rawDataFile.close(); } #else // WRITE_RAWDATAFILE QCOMPARE(currentTestFile.hashLastAuthors, hashLastAuthors.result()); #endif // WRITE_RAWDATAFILE QCryptographicHash hashFilesUrlsDoi(QCryptographicHash::Md5); for (const QString &filesUrlsDoi : const_cast<const QStringList &>(filesUrlsDoiList)) { const QByteArray filesUrlsDoiUtf8 = filesUrlsDoi.toUtf8(); hashFilesUrlsDoi.addData(filesUrlsDoiUtf8); } #ifdef WRITE_RAWDATAFILE if (rawDataFile.open(QFile::Append)) { QTextStream ts(&rawDataFile); ts << QStringLiteral("static const QByteArray ") << filenameStem << QStringLiteral("FilesUrlsDoi = QByteArray::fromHex(\"") << hashFilesUrlsDoi.result().toHex() << QStringLiteral("\");\n"); rawDataFile.close(); } #else // WRITE_RAWDATAFILE QCOMPARE(hashFilesUrlsDoi.result(), currentTestFile.hashFilesUrlsDoi); #endif // WRITE_RAWDATAFILE delete importer; *outFile = bibTeXFile; } void KBibTeXFilesTest::saveFile(File *file, const TestFile &currentTestFile, QString *outFile) { *outFile = QString(); FileExporter *exporter = nullptr; if (currentTestFile.filename.endsWith(QStringLiteral(".bib"))) { FileExporterBibTeX *bibTeXExporter = new FileExporterBibTeX(this); bibTeXExporter->setEncoding(QStringLiteral("utf-8")); exporter = bibTeXExporter; } else { QFAIL(qPrintable(QString::fromLatin1("Don't know format of '%1'").arg(currentTestFile.filename))); } QTemporaryFile tempFile(QDir::tempPath() + "/XXXXXX." + QFileInfo(currentTestFile.filename).fileName()); /// It is the function caller's responsibility to remove the temporary file later tempFile.setAutoRemove(false); QVERIFY(tempFile.open()); QVERIFY(exporter->save(&tempFile, file)); *outFile = tempFile.fileName(); } TestFile KBibTeXFilesTest::createTestFile(const QString &filename #ifndef WRITE_RAWDATAFILE , int numElements, int numEntries, const QString &lastEntryId, const QString &lastEntryLastAuthorLastName, const QByteArray &hashLastAuthors, const QByteArray &hashFilesUrlsDoi #endif // WRITE_RAWDATAFILE ) { TestFile r; r.filename = filename; #ifndef WRITE_RAWDATAFILE r.numElements = numElements; r.numEntries = numEntries; r.lastEntryId = lastEntryId; r.lastEntryLastAuthorLastName = lastEntryLastAuthorLastName; r.hashLastAuthors = hashLastAuthors; r.hashFilesUrlsDoi = hashFilesUrlsDoi; #endif // WRITE_RAWDATAFILE return r; } QTEST_MAIN(KBibTeXFilesTest) #include "kbibtexfilestest.moc" diff --git a/src/test/kbibtexiotest.cpp b/src/test/kbibtexiotest.cpp index 9bfb58cd..ecf01fe6 100644 --- a/src/test/kbibtexiotest.cpp +++ b/src/test/kbibtexiotest.cpp @@ -1,723 +1,723 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include <QtTest> #include <QStandardPaths> #include <KBibTeX> #include <Preferences> #include <Value> #include <Entry> #include <File> #include <FileInfo> #include <EncoderXML> #include <EncoderLaTeX> #include <FileImporter> #include <FileImporterBibTeX> #include <FileImporterRIS> #include <FileExporterBibTeX> #include <FileExporterRIS> #include <FileExporterXML> #include <FileExporterXSLT> #include "logging_test.h" Q_DECLARE_METATYPE(QMimeType) Q_DECLARE_METATYPE(QSharedPointer<Element>) class KBibTeXIOTest : public QObject { Q_OBJECT private slots: void initTestCase(); void encoderConvertToPlainAscii_data(); void encoderConvertToPlainAscii(); void encoderXMLdecode_data(); void encoderXMLdecode(); void encoderXMLencode_data(); void encoderXMLencode(); void encoderLaTeXdecode_data(); void encoderLaTeXdecode(); void encoderLaTeXencode_data(); void encoderLaTeXencode(); void fileImporterSplitName_data(); void fileImporterSplitName(); void fileInfoMimeTypeForUrl_data(); void fileInfoMimeTypeForUrl(); void fileInfoUrlsInText_data(); void fileInfoUrlsInText(); QVector<QPair<const char *, File *> > fileImporterExporterTestCases(); void fileExporterXMLsave_data(); void fileExporterXMLsave(); void fileExporterXSLTstandardSaveFile_data(); void fileExporterXSLTstandardSaveFile(); void fileExporterXSLTstandardSaveElement_data(); void fileExporterXSLTstandardSaveElement(); void fileExporterRISsave_data(); void fileExporterRISsave(); void fileExporterBibTeXsave_data(); void fileExporterBibTeXsave(); void fileImporterRISload_data(); void fileImporterRISload(); void fileImporterBibTeXload_data(); void fileImporterBibTeXload(); void protectiveCasingEntryGeneratedOnTheFly(); void protectiveCasingEntryFromData(); void partialBibTeXInput_data(); void partialBibTeXInput(); void partialRISInput_data(); void partialRISInput(); private: }; void KBibTeXIOTest::encoderConvertToPlainAscii_data() { QTest::addColumn<QString>("unicodestring"); /// Depending on the chosen implementation for Encoder::instance().convertToPlainAscii(), /// the ASCII variant may slightly differ (both alternatives are considered valid). /// If both implementations produce the same ASCII output, 'asciialternative2' is /// to be set to be empty. QTest::addColumn<QString>("asciialternative1"); QTest::addColumn<QString>("asciialternative2"); QTest::newRow("Just 'A'") << QString(QChar(0x00c0)) + QChar(0x00c2) + QChar(0x00c5) << QStringLiteral("AAA") << QString(); QTest::newRow("Just ASCII letters and numbers") << QStringLiteral("qwertyuiopASDFGHJKLzxcvbnm1234567890") << QStringLiteral("qwertyuiopASDFGHJKLzxcvbnm1234567890") << QString(); QTest::newRow("Latin text") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.") << QString(); QTest::newRow("ASCII low and high bytes") << QStringLiteral("\x00\x01\x09\x0a\x10\x11\x19\x1a\x1f\x20\x7e\x7f") << QStringLiteral(" ~") << QString(); QTest::newRow("European Scripts/Latin-1 Supplement") << QString::fromUtf8("\xc3\x80\xc3\x82\xc3\x84\xc3\x92\xc3\x94\xc3\x96\xc3\xac\xc3\xad\xc3\xae\xc3\xaf") << QStringLiteral("AAAOOOiiii") << QStringLiteral("AAAEOOOEiiii"); QTest::newRow("European Scripts/Latin Extended-A") << QString::fromUtf8("\xc4\x8a\xc4\x8b\xc4\xae\xc4\xaf\xc5\x9c\xc5\x9d\xc5\xbb\xc5\xbc") << QStringLiteral("CcIiSsZz") << QString(); QTest::newRow("European Scripts/Latin Extended-B") << QString::fromUtf8("\xc7\x8a\xc7\x8b\xc7\x8c") << QStringLiteral("NJNjnj") << QString(); QTest::newRow("European Scripts/Latin Extended Additional") << QString::fromUtf8("\xe1\xb8\xbe\xe1\xb8\xbf\xe1\xb9\xa4\xe1\xb9\xa5\xe1\xbb\xae\xe1\xbb\xaf") << QStringLiteral("MmSsUu") << QString(); QTest::newRow("European Scripts/Cyrillic") << QString::fromUtf8("\xd0\x90\xd0\x9e\xd0\x9f") << QStringLiteral("AOP") << QString(); QTest::newRow("European Scripts/Greek and Coptic") << QString::fromUtf8("\xce\xba\xce\xb1\xce\xa4\xcf\xba\xce\x9d") << QStringLiteral("kaTSN") << QStringLiteral("kappaalphaTauSanNu"); QTest::newRow("East Asian Scripts/Katakana") << QString::fromUtf8("\xe3\x82\xb7\xe3\x83\x84") << QStringLiteral("shitsu") << QStringLiteral("situ"); QTest::newRow("East Asian Scripts/Hangul Syllables") << QString::fromUtf8("\xea\xb9\x80\xec\xa0\x95\xec\x9d\x80") << QStringLiteral("gimjeongeun") << QStringLiteral("gimjeong-eun"); QTest::newRow("Non-BMP characters (stay unchanged)") << QString::fromUtf8(/* U+10437 */ "\xf0\x90\x90\xb7" /* U+10E6D */ "\xf0\x90\xb9\xad" /* U+1D11E */ "\xf0\x9d\x84\x9e" /* U+10FFFF */ "") << QString::fromUtf8("\xf0\x90\x90\xb7\xf0\x90\xb9\xad\xf0\x9d\x84\x9e") << QString(); QTest::newRow("Base symbols followed by combining symbols") << QString::fromUtf8("123" /* COMBINING GRAVE ACCENT */ "A\xcc\x80" /* COMBINING DIAERESIS */ "A\xcc\x88" /* COMBINING LOW LINE */ "A\xcc\xb2" "123") << QStringLiteral("123AAA123") << QString(); } void KBibTeXIOTest::encoderConvertToPlainAscii() { QFETCH(QString, unicodestring); QFETCH(QString, asciialternative1); QFETCH(QString, asciialternative2); const QString converted = Encoder::instance().convertToPlainAscii(unicodestring); /// Depending on the chosen implementation for Encoder::instance().convertToPlainAscii(), /// the ASCII variant may slightly differ (both alternatives are considered valid). if (converted != asciialternative1 && converted != asciialternative2) qCWarning(LOG_KBIBTEX_TEST) << "converted=" << converted << " asciialternative1=" << asciialternative1 << " asciialternative2=" << asciialternative2; QVERIFY(converted == asciialternative1 || converted == asciialternative2); } void KBibTeXIOTest::encoderXMLdecode_data() { QTest::addColumn<QString>("xml"); QTest::addColumn<QString>("unicode"); QTest::newRow("Just ASCII") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur."); QTest::newRow("Quotation marks") << QStringLiteral("Caesar said: &quot;Veni, vidi, vici&quot;") << QStringLiteral("Caesar said: \"Veni, vidi, vici\""); QTest::newRow("Characters from EncoderXMLCharMapping") << QStringLiteral("&quot;&amp;&lt;&gt;") << QStringLiteral("\"\\&<>"); QTest::newRow("Characters from backslashSymbols") << QStringLiteral("&amp;%_") << QStringLiteral("\\&\\%\\_"); for (int start = 0; start < 16; ++start) { QString xmlString, unicodeString; for (int offset = 1561; offset < 6791; offset += 621) { const ushort unicode = static_cast<ushort>((start * 3671 + offset) & 0x7fff); xmlString += QStringLiteral("&#") + QString::number(unicode) + QStringLiteral(";"); unicodeString += QChar(unicode); } QTest::newRow(QString(QStringLiteral("Some arbitrary Unicode characters (%1): %2")).arg(start).arg(xmlString).toLatin1().constData()) << xmlString << unicodeString; } } void KBibTeXIOTest::encoderXMLdecode() { QFETCH(QString, xml); QFETCH(QString, unicode); QCOMPARE(EncoderXML::instance().decode(xml), unicode); } void KBibTeXIOTest::encoderXMLencode_data() { encoderXMLdecode_data(); } void KBibTeXIOTest::encoderXMLencode() { QFETCH(QString, xml); QFETCH(QString, unicode); - QCOMPARE(EncoderXML::instance().encode(unicode, Encoder::TargetEncodingASCII), xml); + QCOMPARE(EncoderXML::instance().encode(unicode, Encoder::TargetEncoding::ASCII), xml); } void KBibTeXIOTest::encoderLaTeXdecode_data() { QTest::addColumn<QString>("latex"); QTest::addColumn<QString>("unicode"); QTest::addColumn<QString>("alternativelatex"); QTest::newRow("Just ASCII") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.") << QStringLiteral("Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur.") << QString(); QTest::newRow("Dotless i and j characters") << QStringLiteral("{\\`\\i}{\\'\\i}{\\^\\i}{\\\"\\i}{\\~\\i}{\\=\\i}{\\u\\i}{\\k\\i}{\\^\\j}{\\v\\i}{\\v\\j}") << QString(QChar(0x00EC)) + QChar(0x00ED) + QChar(0x00EE) + QChar(0x00EF) + QChar(0x0129) + QChar(0x012B) + QChar(0x012D) + QChar(0x012F) + QChar(0x0135) + QChar(0x01D0) + QChar(0x01F0) << QString(); QTest::newRow("\\l and \\ldots") << QStringLiteral("\\l\\ldots\\l\\ldots") << QString(QChar(0x0142)) + QChar(0x2026) + QChar(0x0142) + QChar(0x2026) << QStringLiteral("{\\l}{\\ldots}{\\l}{\\ldots}"); } void KBibTeXIOTest::encoderLaTeXdecode() { QFETCH(QString, latex); QFETCH(QString, unicode); QCOMPARE(EncoderLaTeX::instance().decode(latex), unicode); } void KBibTeXIOTest::encoderLaTeXencode_data() { encoderLaTeXdecode_data(); } void KBibTeXIOTest::encoderLaTeXencode() { QFETCH(QString, latex); QFETCH(QString, unicode); QFETCH(QString, alternativelatex); - const QString generatedLatex = EncoderLaTeX::instance().encode(unicode, Encoder::TargetEncodingASCII); + const QString generatedLatex = EncoderLaTeX::instance().encode(unicode, Encoder::TargetEncoding::ASCII); if (generatedLatex != latex && !alternativelatex.isEmpty()) QCOMPARE(generatedLatex, alternativelatex); else QCOMPARE(generatedLatex, latex); } void KBibTeXIOTest::fileImporterSplitName_data() { QTest::addColumn<QString>("name"); QTest::addColumn<Person *>("person"); QTest::newRow("Empty name") << QString() << new Person(QString(), QString(), QString()); QTest::newRow("PubMed style") << QStringLiteral("Jones A B C") << new Person(QStringLiteral("A B C"), QStringLiteral("Jones"), QString()); QTest::newRow("Just last name") << QStringLiteral("Dido") << new Person(QString(), QStringLiteral("Dido"), QString()); QTest::newRow("Name with 'von'") << QStringLiteral("Theodor von Sickel") << new Person(QStringLiteral("Theodor"), QStringLiteral("von Sickel"), QString()); QTest::newRow("Name with 'von', reversed") << QStringLiteral("von Sickel, Theodor") << new Person(QStringLiteral("Theodor"), QStringLiteral("von Sickel"), QString()); QTest::newRow("Name with 'van der'") << QStringLiteral("Adriaen van der Werff") << new Person(QStringLiteral("Adriaen"), QStringLiteral("van der Werff"), QString()); QTest::newRow("Name with 'van der', reversed") << QStringLiteral("van der Werff, Adriaen") << new Person(QStringLiteral("Adriaen"), QStringLiteral("van der Werff"), QString()); QTest::newRow("Name with suffix") << QStringLiteral("Anna Eleanor Roosevelt Jr.") << new Person(QStringLiteral("Anna Eleanor"), QStringLiteral("Roosevelt"), QStringLiteral("Jr.")); } void KBibTeXIOTest::fileImporterSplitName() { QFETCH(QString, name); QFETCH(Person *, person); Person *computedPerson = FileImporter::splitName(name); QCOMPARE(*computedPerson, *person); delete person; delete computedPerson; } void KBibTeXIOTest::fileInfoMimeTypeForUrl_data() { QTest::addColumn<QUrl>("url"); QTest::addColumn<QMimeType>("mimetype"); static const QMimeDatabase db; QTest::newRow("Invalid URL") << QUrl() << QMimeType(); QTest::newRow("Generic URL") << QUrl(QStringLiteral("https://www.example.com")) << db.mimeTypeForName(QStringLiteral("text/html")); QTest::newRow("Generic local file") << QUrl(QStringLiteral("/usr/bin/who")) << db.mimeTypeForName(QStringLiteral("application/octet-stream")); QTest::newRow("Generic Samba URL") << QUrl(QStringLiteral("smb://fileserver.local/file")) << db.mimeTypeForName(QStringLiteral("application/octet-stream")); QTest::newRow("URL to .bib file") << QUrl(QStringLiteral("https://www.example.com/references.bib")) << db.mimeTypeForName(QStringLiteral("text/x-bibtex")); QTest::newRow("Local .bib file") << QUrl(QStringLiteral("/home/user/references.bib")) << db.mimeTypeForName(QStringLiteral("text/x-bibtex")); QTest::newRow("URL to .pdf file") << QUrl(QStringLiteral("https://www.example.com/references.pdf")) << db.mimeTypeForName(QStringLiteral("application/pdf")); QTest::newRow("Local .pdf file") << QUrl(QStringLiteral("/home/user/references.pdf")) << db.mimeTypeForName(QStringLiteral("application/pdf")); } void KBibTeXIOTest::fileInfoMimeTypeForUrl() { QFETCH(QUrl, url); QFETCH(QMimeType, mimetype); QCOMPARE(FileInfo::mimeTypeForUrl(url), mimetype); } void KBibTeXIOTest::fileInfoUrlsInText_data() { QTest::addColumn<QString>("text"); QTest::addColumn<QSet<QUrl>>("expectedUrls"); QTest::newRow("Empty text") << QString() << QSet<QUrl>(); QTest::newRow("Lore ipsum with DOI (without URL)") << QStringLiteral("Lore ipsum 10.1000/38-abc Lore ipsum") << QSet<QUrl> {QUrl(KBibTeX::doiUrlPrefix + QStringLiteral("10.1000/38-abc"))}; QTest::newRow("Lore ipsum with DOI (with URL)") << QStringLiteral("Lore ipsum http://doi.example.org/10.1000/38-abc Lore ipsum") << QSet<QUrl> {QUrl(KBibTeX::doiUrlPrefix + QStringLiteral("10.1000/38-abc"))}; QTest::newRow("URLs and DOI (without URL), all semicolon-separated") << QStringLiteral("http://www.example.com;10.1000/38-abc ;\nhttps://www.example.com") << QSet<QUrl> {QUrl(QStringLiteral("http://www.example.com")), QUrl(KBibTeX::doiUrlPrefix + QStringLiteral("10.1000/38-abc")), QUrl(QStringLiteral("https://www.example.com"))}; QTest::newRow("URLs and DOI (with URL), all semicolon-separated") << QStringLiteral("http://www.example.com\n; 10.1000/38-abc;https://www.example.com") << QSet<QUrl> {QUrl(QStringLiteral("http://www.example.com")), QUrl(KBibTeX::doiUrlPrefix + QStringLiteral("10.1000/38-abc")), QUrl(QStringLiteral("https://www.example.com"))}; QTest::newRow("URLs with various separators") << QStringLiteral("http://www.example.com/def.pdf https://www.example.com\nhttp://download.example.com/abc") << QSet<QUrl> {QUrl(QStringLiteral("http://www.example.com/def.pdf")), QUrl(QStringLiteral("https://www.example.com")), QUrl(QStringLiteral("http://download.example.com/abc"))}; QTest::newRow("URLs with query strings and anchors") << QStringLiteral("http://www.example.com/def.pdf?a=3&b=1 https://www.example.com#1581584\nhttp://download.example.com/abc,7352,A#abc?gh=352&ghi=1254") << QSet<QUrl> {QUrl(QStringLiteral("http://www.example.com/def.pdf?a=3&b=1")), QUrl(QStringLiteral("https://www.example.com#1581584")), QUrl(QStringLiteral("http://download.example.com/abc,7352,A#abc?gh=352&ghi=1254"))}; } void KBibTeXIOTest::fileInfoUrlsInText() { QFETCH(QString, text); QFETCH(QSet<QUrl>, expectedUrls); QSet<QUrl> extractedUrls; - FileInfo::urlsInText(text, FileInfo::TestExistenceNo, QString(), extractedUrls); + FileInfo::urlsInText(text, FileInfo::TestExistence::No, QString(), extractedUrls); QCOMPARE(extractedUrls.count(), expectedUrls.count()); for (const QUrl &expectedUrl : const_cast<const QSet<QUrl> &>(expectedUrls)) QCOMPARE(extractedUrls.contains(expectedUrl), true); } static const char *fileImporterExporterTestCases_Label_Empty_file = "Empty file"; static const char *fileImporterExporterTestCases_Label_Moby_Dick = "Moby Dick"; QVector<QPair<const char *, File *> > KBibTeXIOTest::fileImporterExporterTestCases() { /// The vector 'result' is static so that if this function is invoked multiple /// times, the vector will be initialized and filled with File objects only upon /// the function's first invocation. static QVector<QPair<const char *, File *> > result; if (result.isEmpty()) { /// Empty file without any entries result.append(QPair<const char *, File *>(fileImporterExporterTestCases_Label_Empty_file, new File())); /// File with single entry, inspired by 'Moby Dick' File *f1 = new File(); QSharedPointer<Entry> entry1(new Entry(Entry::etArticle, QStringLiteral("the-whale-1851"))); f1->append(entry1); entry1->insert(Entry::ftTitle, Value() << QSharedPointer<PlainText>(new PlainText(QStringLiteral("{Call me Ishmael}")))); entry1->insert(Entry::ftAuthor, Value() << QSharedPointer<Person>(new Person(QStringLiteral("Herman"), QStringLiteral("Melville"))) << QSharedPointer<Person>(new Person(QStringLiteral("Moby"), QStringLiteral("Dick")))); entry1->insert(Entry::ftYear, Value() << QSharedPointer<PlainText>(new PlainText(QStringLiteral("1851")))); result.append(QPair<const char *, File *>(fileImporterExporterTestCases_Label_Moby_Dick, f1)); // TODO add more file objects to result vector /// Set various properties to guarantee reproducible results irrespective of local settings for (auto it = result.constBegin(); it != result.constEnd(); ++it) { File *file = it->second; file->setProperty(File::NameFormatting, Preferences::personNameFormatLastFirst); file->setProperty(File::ProtectCasing, static_cast<int>(Qt::Checked)); // TODO more file properties to set? } } return result; } void KBibTeXIOTest::fileExporterXMLsave_data() { QTest::addColumn<File *>("bibTeXfile"); QTest::addColumn<QString>("xmlData"); static const QHash<const char *, QString> keyToXmlData { {fileImporterExporterTestCases_Label_Empty_file, QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>|<!-- XML document written by KBibTeXIO as part of KBibTeX -->|<!-- https://userbase.kde.org/KBibTeX -->|<bibliography>|</bibliography>|")}, {fileImporterExporterTestCases_Label_Moby_Dick, QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?>|<!-- XML document written by KBibTeXIO as part of KBibTeX -->|<!-- https://userbase.kde.org/KBibTeX -->|<bibliography>| <entry id=\"the-whale-1851\" type=\"article\">| <authors>|<person><firstname>Herman</firstname><lastname>Melville</lastname></person> <person><firstname>Moby</firstname><lastname>Dick</lastname></person>| </authors>| <title><text>Call me Ishmael</text></title>| <year><text>1851</text></year>| </entry>|</bibliography>|")} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToXmlData.contains(it->first)) QTest::newRow(it->first) << it->second << keyToXmlData.value(it->first); } void KBibTeXIOTest::fileExporterXMLsave() { QFETCH(File *, bibTeXfile); QFETCH(QString, xmlData); FileExporterXML fileExporterXML(this); QStringList errorLog; const QString generatedData = fileExporterXML.toString(bibTeXfile, &errorLog).remove(QLatin1Char('\r')).replace(QLatin1Char('\n'), QLatin1Char('|')); for (const QString &logLine : const_cast<const QStringList &>(errorLog)) qCDebug(LOG_KBIBTEX_TEST) << logLine; QCOMPARE(generatedData, xmlData); } void KBibTeXIOTest::fileExporterXSLTstandardSaveFile_data() { QTest::addColumn<File *>("bibTeXfile"); QTest::addColumn<QSet<QString>>("expectedFragments"); static const QHash<const char *, QSet<QString>> keyToXsltData { {fileImporterExporterTestCases_Label_Empty_file, {QStringLiteral("<title>Bibliography</title>"), QStringLiteral("<body/>")}}, {fileImporterExporterTestCases_Label_Moby_Dick, {QStringLiteral("<title>Bibliography</title>"), QStringLiteral(">1851<"), QStringLiteral(">Call me Ishmael<"), QStringLiteral("</b>"), QStringLiteral("</body>")}} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToXsltData.contains(it->first)) QTest::newRow(it->first) << it->second << keyToXsltData.value(it->first); } void KBibTeXIOTest::fileExporterXSLTstandardSaveFile() { QFETCH(File *, bibTeXfile); QFETCH(QSet<QString>, expectedFragments); FileExporterXSLT fileExporterXSLT(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kbibtex/standard.xsl")), this); QStringList errorLog; const QString generatedData = fileExporterXSLT.toString(bibTeXfile, &errorLog).remove(QLatin1Char('\r')).replace(QLatin1Char('\n'), QLatin1Char('|')); for (const QString &logLine : const_cast<const QStringList &>(errorLog)) qCDebug(LOG_KBIBTEX_TEST) << logLine; for (const QString &fragment : expectedFragments) QVERIFY2(generatedData.contains(fragment), QString(QStringLiteral("Fragment '%1' not found in generated XML data")).arg(fragment).toLatin1().constData()); } void KBibTeXIOTest::fileExporterXSLTstandardSaveElement_data() { QTest::addColumn<QSharedPointer<Element>>("element"); QTest::addColumn<QSet<QString>>("expectedFragments"); static const QHash<const char *, QSet<QString>> keyToXsltData { {fileImporterExporterTestCases_Label_Moby_Dick, {QStringLiteral("<title>Bibliography</title>"), QStringLiteral(">1851<"), QStringLiteral(">Call me Ishmael<"), QStringLiteral("</b>"), QStringLiteral("</body>")}} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (!it->second->isEmpty() && keyToXsltData.contains(it->first)) QTest::newRow(it->first) << it->second->first() << keyToXsltData.value(it->first); } void KBibTeXIOTest::fileExporterXSLTstandardSaveElement() { QFETCH(QSharedPointer<Element>, element); QFETCH(QSet<QString>, expectedFragments); FileExporterXSLT fileExporterXSLT(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kbibtex/standard.xsl")), this); QStringList errorLog; const QString generatedData = fileExporterXSLT.toString(element, nullptr, &errorLog).remove(QLatin1Char('\r')).replace(QLatin1Char('\n'), QLatin1Char('|')); for (const QString &logLine : const_cast<const QStringList &>(errorLog)) qCDebug(LOG_KBIBTEX_TEST) << logLine; for (const QString &fragment : expectedFragments) QVERIFY2(generatedData.contains(fragment), QString(QStringLiteral("Fragment '%1' not found in generated XML data")).arg(fragment).toLatin1().constData()); } void KBibTeXIOTest::fileExporterRISsave_data() { QTest::addColumn<File *>("bibTeXfile"); QTest::addColumn<QString>("risData"); static const QHash<const char *, QString> keyToRisData { {fileImporterExporterTestCases_Label_Empty_file, QString()}, {fileImporterExporterTestCases_Label_Moby_Dick, QStringLiteral("TY - JOUR|ID - the-whale-1851|AU - Melville, Herman|AU - Dick, Moby|TI - Call me Ishmael|PY - 1851///|ER - ||")} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToRisData.contains(it->first)) QTest::newRow(it->first) << it->second << keyToRisData.value(it->first); } void KBibTeXIOTest::fileExporterRISsave() { QFETCH(File *, bibTeXfile); QFETCH(QString, risData); FileExporterRIS fileExporterRIS(this); QStringList errorLog; const QString generatedData = fileExporterRIS.toString(bibTeXfile, &errorLog).remove(QLatin1Char('\r')).replace(QLatin1Char('\n'), QLatin1Char('|')); for (const QString &logLine : const_cast<const QStringList &>(errorLog)) qCDebug(LOG_KBIBTEX_TEST) << logLine; QCOMPARE(generatedData, risData); } void KBibTeXIOTest::fileExporterBibTeXsave_data() { QTest::addColumn<File *>("bibTeXfile"); QTest::addColumn<QString>("bibTeXdata"); static const QHash<const char *, QString> keyToBibTeXData { {fileImporterExporterTestCases_Label_Empty_file, QString()}, {fileImporterExporterTestCases_Label_Moby_Dick, QStringLiteral("@article{the-whale-1851,|\tauthor = {Melville, Herman and Dick, Moby},|\ttitle = {{Call me Ishmael}},|\tyear = {1851}|}||")} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToBibTeXData.contains(it->first)) QTest::newRow(it->first) << it->second << keyToBibTeXData.value(it->first); } void KBibTeXIOTest::fileExporterBibTeXsave() { QFETCH(File *, bibTeXfile); QFETCH(QString, bibTeXdata); FileExporterBibTeX fileExporterBibTeX(this); QStringList errorLog; const QString generatedData = fileExporterBibTeX.toString(bibTeXfile, &errorLog).remove(QLatin1Char('\r')).replace(QLatin1Char('\n'), QLatin1Char('|')); for (const QString &logLine : const_cast<const QStringList &>(errorLog)) qCDebug(LOG_KBIBTEX_TEST) << logLine; QCOMPARE(generatedData, bibTeXdata); } void KBibTeXIOTest::fileImporterRISload_data() { QTest::addColumn<QByteArray>("risData"); QTest::addColumn<File *>("bibTeXfile"); static const QHash<const char *, QString> keyToRisData { {fileImporterExporterTestCases_Label_Empty_file, QString()}, {fileImporterExporterTestCases_Label_Moby_Dick, QStringLiteral("TY - JOUR|ID - the-whale-1851|AU - Melville, Herman|AU - Dick, Moby|TI - Call me Ishmael|PY - 1851///|ER - ||")} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToRisData.contains(it->first)) QTest::newRow(it->first) << keyToRisData.value(it->first).toUtf8().replace('|', '\n') << it->second; } void KBibTeXIOTest::fileImporterRISload() { QFETCH(QByteArray, risData); QFETCH(File *, bibTeXfile); FileImporterRIS fileImporterRIS(this); fileImporterRIS.setProtectCasing(true); QBuffer buffer(&risData); buffer.open(QBuffer::ReadOnly); QScopedPointer<File> generatedFile(fileImporterRIS.load(&buffer)); QVERIFY(generatedFile->operator ==(*bibTeXfile)); } void KBibTeXIOTest::fileImporterBibTeXload_data() { QTest::addColumn<QByteArray>("bibTeXdata"); QTest::addColumn<File *>("bibTeXfile"); static const QHash<const char *, QString> keyToBibTeXData { {fileImporterExporterTestCases_Label_Empty_file, QString()}, {fileImporterExporterTestCases_Label_Moby_Dick, QStringLiteral("@article{the-whale-1851,|\tauthor = {Melville, Herman and Dick, Moby},|\ttitle = {{Call me Ishmael}},|\tyear = {1851}|}||")} }; static const QVector<QPair<const char *, File *> > keyFileTable = fileImporterExporterTestCases(); for (auto it = keyFileTable.constBegin(); it != keyFileTable.constEnd(); ++it) if (keyToBibTeXData.contains(it->first)) QTest::newRow(it->first) << keyToBibTeXData.value(it->first).toUtf8().replace('|', '\n') << it->second ; } void KBibTeXIOTest::fileImporterBibTeXload() { QFETCH(QByteArray, bibTeXdata); QFETCH(File *, bibTeXfile); FileImporterBibTeX fileImporterBibTeX(this); QBuffer buffer(&bibTeXdata); buffer.open(QBuffer::ReadOnly); QScopedPointer<File> generatedFile(fileImporterBibTeX.load(&buffer)); QVERIFY(generatedFile->operator ==(*bibTeXfile)); } void KBibTeXIOTest::protectiveCasingEntryGeneratedOnTheFly() { static const QString titleText = QStringLiteral("Some Title for a Journal Article"); static const QString singleCurleyBracketTitle = QStringLiteral("{") + titleText + QStringLiteral("}"); static const QString doubleCurleyBracketTitle = QStringLiteral("{{") + titleText + QStringLiteral("}}"); FileExporterBibTeX fileExporterBibTeX(this); /// Create a simple File object with a title field File file; file.setProperty(File::StringDelimiter, QStringLiteral("{}")); QSharedPointer<Entry> entry {new Entry(Entry::etArticle, QStringLiteral("SomeId"))}; Value titleValue = Value() << QSharedPointer<PlainText>(new PlainText(titleText)); entry->insert(Entry::ftTitle, titleValue); file.append(entry); file.setProperty(File::ProtectCasing, Qt::Checked); const QString textWithProtectiveCasing = fileExporterBibTeX.toString(&file); QVERIFY(textWithProtectiveCasing.contains(doubleCurleyBracketTitle)); file.setProperty(File::ProtectCasing, Qt::Unchecked); const QString textWithoutProtectiveCasing = fileExporterBibTeX.toString(&file); QVERIFY(textWithoutProtectiveCasing.contains(singleCurleyBracketTitle) && !textWithoutProtectiveCasing.contains(doubleCurleyBracketTitle)); } void KBibTeXIOTest::protectiveCasingEntryFromData() { static const QString titleText = QStringLiteral("Some Title for a Journal Article"); static const QString singleCurleyBracketTitle = QStringLiteral("{") + titleText + QStringLiteral("}"); static const QString doubleCurleyBracketTitle = QStringLiteral("{{") + titleText + QStringLiteral("}}"); static const QString bibTeXDataDoubleCurleyBracketTitle = QStringLiteral("@articl{doubleCurleyBracketTitle,\ntitle={{") + titleText + QStringLiteral("}}\n}\n"); static const QString bibTeXDataSingleCurleyBracketTitle = QStringLiteral("@articl{singleCurleyBracketTitle,\ntitle={") + titleText + QStringLiteral("}\n}\n"); FileImporterBibTeX fileImporterBibTeX(this); FileExporterBibTeX fileExporterBibTeX(this); QByteArray b1(bibTeXDataDoubleCurleyBracketTitle.toUtf8()); QBuffer bufferDoubleCurleyBracketTitle(&b1, this); QByteArray b2(bibTeXDataSingleCurleyBracketTitle.toUtf8()); QBuffer bufferSingleCurleyBracketTitle(&b2, this); bufferDoubleCurleyBracketTitle.open(QBuffer::ReadOnly); QScopedPointer<File> fileDoubleCurleyBracketTitle(fileImporterBibTeX.load(&bufferDoubleCurleyBracketTitle)); bufferDoubleCurleyBracketTitle.close(); fileDoubleCurleyBracketTitle->setProperty(File::StringDelimiter, QStringLiteral("{}")); bufferSingleCurleyBracketTitle.open(QBuffer::ReadOnly); QScopedPointer<File> fileSingleCurleyBracketTitle(fileImporterBibTeX.load(&bufferSingleCurleyBracketTitle)); bufferSingleCurleyBracketTitle.close(); fileSingleCurleyBracketTitle->setProperty(File::StringDelimiter, QStringLiteral("{}")); fileDoubleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::PartiallyChecked); const QString textDoubleCurleyBracketTitlePartialProtectiveCasing = fileExporterBibTeX.toString(fileDoubleCurleyBracketTitle.data()); QVERIFY(textDoubleCurleyBracketTitlePartialProtectiveCasing.contains(doubleCurleyBracketTitle)); fileSingleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::PartiallyChecked); const QString textSingleCurleyBracketTitlePartialProtectiveCasing = fileExporterBibTeX.toString(fileSingleCurleyBracketTitle.data()); QVERIFY(textSingleCurleyBracketTitlePartialProtectiveCasing.contains(singleCurleyBracketTitle) && !textSingleCurleyBracketTitlePartialProtectiveCasing.contains(doubleCurleyBracketTitle)); fileDoubleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::Checked); const QString textDoubleCurleyBracketTitleWithProtectiveCasing = fileExporterBibTeX.toString(fileDoubleCurleyBracketTitle.data()); QVERIFY(textDoubleCurleyBracketTitleWithProtectiveCasing.contains(doubleCurleyBracketTitle)); fileSingleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::Checked); const QString textSingleCurleyBracketTitleWithProtectiveCasing = fileExporterBibTeX.toString(fileSingleCurleyBracketTitle.data()); QVERIFY(textSingleCurleyBracketTitleWithProtectiveCasing.contains(doubleCurleyBracketTitle)); fileDoubleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::Unchecked); const QString textDoubleCurleyBracketTitleWithoutProtectiveCasing = fileExporterBibTeX.toString(fileDoubleCurleyBracketTitle.data()); QVERIFY(textDoubleCurleyBracketTitleWithoutProtectiveCasing.contains(singleCurleyBracketTitle) && !textDoubleCurleyBracketTitleWithoutProtectiveCasing.contains(doubleCurleyBracketTitle)); fileSingleCurleyBracketTitle->setProperty(File::ProtectCasing, Qt::Unchecked); const QString textSingleCurleyBracketTitleWithoutProtectiveCasing = fileExporterBibTeX.toString(fileSingleCurleyBracketTitle.data()); QVERIFY(textSingleCurleyBracketTitleWithoutProtectiveCasing.contains(singleCurleyBracketTitle) && !textSingleCurleyBracketTitleWithoutProtectiveCasing.contains(doubleCurleyBracketTitle)); } void KBibTeXIOTest::partialBibTeXInput_data() { QTest::addColumn<bool>("isValid"); QTest::addColumn<QString>("text"); static const struct BibTeXDataTable { const char *label; const bool isValid; const QString text; } bibTeXDataTable[] = { {"Empty string", false, QString()}, {"Only 'at' sign", false, QStringLiteral("@")}, {"Only 'at' sign followed by element type", false, QStringLiteral("@entry")}, {"Only up to opening curly bracket", false, QStringLiteral("@entry{")}, {"Complete entry but without id", true, QStringLiteral("@entry{,\n title=\"{Abc Def}\",\n month = jan\n}")}, {"Entry without any data", true, QStringLiteral("@entry{}")}, {"Entry up to entry id, but no closing curly bracket", false, QStringLiteral("@entry{test")}, {"Entry up to entry id with opening curly bracket", false, QStringLiteral("@entry{test{")}, {"Entry up to entry id with closing curly bracket", true, QStringLiteral("@entry{test}")}, {"Entry up to comma after entry id", false, QStringLiteral("@entry{test,")}, {"Entry up to comma after entry id, followed by closing curly bracket", true, QStringLiteral("@entry{test,}")}, {"Entry up to first field's key, but nothing more, not even an assign char", false, QStringLiteral("@entry{test,title")}, {"Entry up to first field's key, but nothing more, just a closing curly bracket", false, QStringLiteral("@entry{test,title}")}, {"Entry up to first field's assign char, but nothing more", false, QStringLiteral("@entry{test,title=")}, {"Entry up to first field's assign char, but nothing more, just a closing curly bracket", false, QStringLiteral("@entry{test,title=}")}, {"Invalid combination of curly bracket in a field's value (1)", false, QStringLiteral("@entry{test,title={}")}, {"Invalid combination of curly bracket in a field's value (2)", false, QStringLiteral("@entry{test,title={{}}")}, {"Invalid combination of curly bracket in a field's value (3)", false, QStringLiteral("@entry{test,title={}{}")}, {"Invalid combination of curly bracket in a field's value (4)", false, QStringLiteral("@entry{test,title={}{}}")}, {"Complete entry with empty title (1)", true, QStringLiteral("@entry{test,\n title=\"{}\"\n}")}, {"Complete entry with empty title (2)", true, QStringLiteral("@entry{test,\n title=\"\"\n}")}, {"Complete entry with empty title (3)", true, QStringLiteral("@entry{test,\n title={{}}\n}")}, {"Complete entry with empty title (4)", true, QStringLiteral("@entry{test,\n title={}\n}")}, {"Entry abruptly ending at macro key as field value (1)", false, QStringLiteral("@entry{test,\n month = jan")}, {"Entry abruptly ending at macro key as field value (2)", false, QStringLiteral("@entry{test,\n month = jan\n")}, // TODO more tests {"Complete entry", true, QStringLiteral("@entry{test,\n title=\"{Abc Def}\",\n month = jan\n}")} }; for (const auto &bibTeXDataRow : bibTeXDataTable) QTest::newRow(bibTeXDataRow.label) << bibTeXDataRow.isValid << bibTeXDataRow.text; } void KBibTeXIOTest::partialBibTeXInput() { QFETCH(bool, isValid); QFETCH(QString, text); bool gotErrors = false; FileImporterBibTeX importer(this); connect(&importer, &FileImporter::message, [&gotErrors](const FileImporter::MessageSeverity messageSeverity, const QString &messageText) { - gotErrors |= messageSeverity >= FileImporter::SeverityError; + gotErrors |= messageSeverity >= FileImporter::MessageSeverity::Error; Q_UNUSED(messageText) //qCDebug(LOG_KBIBTEX_TEST)<<"FileImporterBibTeX issues message during 'partialBibTeXInput' test: "<<messageText; }); QScopedPointer<File> bibTeXfile(importer.fromString(text)); QVERIFY(text.isEmpty() || isValid != gotErrors); QVERIFY(isValid ? (!bibTeXfile.isNull() && bibTeXfile->count() == 1) : (bibTeXfile.isNull() || bibTeXfile->count() == 0)); } void KBibTeXIOTest::partialRISInput_data() { QTest::addColumn<bool>("isValid"); QTest::addColumn<QString>("text"); static const struct RISDataTable { const char *label; const bool isValid; const QString text; } risDataTable[] = { //{"Empty string", false, QString()}, {"Incorrect year", true, QStringLiteral("TY - JOUR\nAU - Shannon, Claude E.\nPY - 5555/07//\nTI - A Mathematical Theory of Communication\nT2 - Bell System Technical Journal\nSP - 379\nEP - 423\nVL - 27\nER -")}, {"Incorrect month", true, QStringLiteral("TY - JOUR\nAU - Shannon, Claude E.\nPY - 1948/17//\nTI - A Mathematical Theory of Communication\nT2 - Bell System Technical Journal\nSP - 379\nEP - 423\nVL - 27\nER -")}, {"Entry does not end with 'ER'", true, QStringLiteral("TY - JOUR\nAU - Shannon, Claude E.\nPY - 1948/07//\nTI - A Mathematical Theory of Communication\nT2 - Bell System Technical Journal\nSP - 379\nEP - 423\nVL - 27")}, // TODO more tests //{"Complete entry", true, QStringLiteral("TY - JOUR\nAU - Shannon, Claude E.\nPY - 1948/07//\nTI - A Mathematical Theory of Communication\nT2 - Bell System Technical Journal\nSP - 379\nEP - 423\nVL - 27\nER -")} }; for (const auto &risDataRow : risDataTable) QTest::newRow(risDataRow.label) << risDataRow.isValid << risDataRow.text; } void KBibTeXIOTest::partialRISInput() { QFETCH(bool, isValid); QFETCH(QString, text); bool gotErrors = false; FileImporterRIS importer(this); connect(&importer, &FileImporter::message, [&gotErrors](const FileImporter::MessageSeverity messageSeverity, const QString &messageText) { - gotErrors |= messageSeverity >= FileImporter::SeverityError; + gotErrors |= messageSeverity >= FileImporter::MessageSeverity::Error; Q_UNUSED(messageText) //qCDebug(LOG_KBIBTEX_TEST)<<"FileImporterRIS issues message during 'partialBibTeXInput' test: "<<messageText; }); QScopedPointer<File> bibTeXfile(importer.fromString(text)); QVERIFY(text.isEmpty() || isValid != gotErrors); QVERIFY(isValid ? (!bibTeXfile.isNull() && bibTeXfile->count() == 1) : (bibTeXfile.isNull() || bibTeXfile->count() == 0)); } void KBibTeXIOTest::initTestCase() { qRegisterMetaType<FileImporter::MessageSeverity>(); } QTEST_MAIN(KBibTeXIOTest) #include "kbibtexiotest.moc" diff --git a/src/test/kbibtexnetworkingtest.cpp b/src/test/kbibtexnetworkingtest.cpp index a10c7c11..04e0659d 100644 --- a/src/test/kbibtexnetworkingtest.cpp +++ b/src/test/kbibtexnetworkingtest.cpp @@ -1,284 +1,284 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include <QtTest> #include <onlinesearch/OnlineSearchAbstract> #include <AssociatedFiles> Q_DECLARE_METATYPE(AssociatedFiles::PathType) typedef QMap<QString, QString> FormData; class OnlineSearchDummy : public OnlineSearchAbstract { Q_OBJECT public: explicit OnlineSearchDummy(QObject *parent); void startSearch(const QMap<QString, QString> &query, int numResults) override; QString label() const override; QUrl homepage() const override; QMap<QString, QString> formParameters_public(const QString &htmlText, int startPos); void sanitizeEntry_public(QSharedPointer<Entry> entry); protected: QString favIconUrl() const override; }; class KBibTeXNetworkingTest : public QObject { Q_OBJECT private slots: void initTestCase(); void onlineSearchAbstractFormParameters_data(); void onlineSearchAbstractFormParameters(); void onlineSearchAbstractSanitizeEntry_data(); void onlineSearchAbstractSanitizeEntry(); void associatedFilescomputeAssociateURL_data(); void associatedFilescomputeAssociateURL(); private: }; OnlineSearchDummy::OnlineSearchDummy(QObject *parent) : OnlineSearchAbstract(parent) { /// nothing } void OnlineSearchDummy::startSearch(const QMap<QString, QString> &query, int numResults) { Q_UNUSED(query) Q_UNUSED(numResults) } QString OnlineSearchDummy::label() const { return QStringLiteral("Dummy Search"); } QUrl OnlineSearchDummy::homepage() const { return QUrl::fromUserInput(QStringLiteral("https://www.kde.org")); } QString OnlineSearchDummy::favIconUrl() const { return QStringLiteral("https://www.kde.org/favicon.ico"); } QMap<QString, QString> OnlineSearchDummy::formParameters_public(const QString &htmlText, int startPos) { return formParameters(htmlText, startPos); } void OnlineSearchDummy::sanitizeEntry_public(QSharedPointer<Entry> entry) { sanitizeEntry(entry); } void KBibTeXNetworkingTest::onlineSearchAbstractFormParameters_data() { QTest::addColumn<QString>("htmlCode"); QTest::addColumn<int>("startPos"); QTest::addColumn<FormData>("expectedResult"); QTest::newRow("Empty Form (1)") << QString() << 0 << QMap<QString, QString>(); QTest::newRow("Empty Form (2)") << QStringLiteral("<form></form>") << 0 << QMap<QString, QString>(); QTest::newRow("Form with text") << QStringLiteral("<form><input type=\"text\" name=\"abc\" value=\"ABC\" /></form>") << 0 << QMap<QString, QString> {{QStringLiteral("abc"), QStringLiteral("ABC")}}; QTest::newRow("Form with text but without quotation marks") << QStringLiteral("<form><input type=text name=abc value=ABC /></form>") << 0 << QMap<QString, QString> {{QStringLiteral("abc"), QStringLiteral("ABC")}}; QTest::newRow("Form with text and single quotation marks") << QStringLiteral("<form><input type='text' name='abc' value='ABC' /></form>") << 0 << QMap<QString, QString> {{QStringLiteral("abc"), QStringLiteral("ABC")}}; QTest::newRow("Form with radio button (none selected)") << QStringLiteral("<form><input type=\"radio\" name=\"direction\" value=\"right\" /><input type=\"radio\" name=\"direction\" value=\"left\"/></form>") << 0 << QMap<QString, QString>(); QTest::newRow("Form with radio button (old-style)") << QStringLiteral("<form><input type=\"radio\" name=\"direction\" value=\"right\" /><input type=\"radio\" name=\"direction\" value=\"left\" checked/></form>") << 0 << QMap<QString, QString> {{QStringLiteral("direction"), QStringLiteral("left")}}; QTest::newRow("Form with radio button (modern)") << QStringLiteral("<form><input type=\"radio\" name=\"direction\" value=\"right\" checked=\"checked\"/><input type=\"radio\" name=\"direction\" value=\"left\"/></form>") << 0 << QMap<QString, QString> {{QStringLiteral("direction"), QStringLiteral("right")}}; QTest::newRow("Form with select/option (none selected)") << QStringLiteral("<form><select name=\"direction\"><option value=\"left\">Left</option><option value=\"right\">Right</option></select></form>") << 0 << QMap<QString, QString>(); QTest::newRow("Form with select/option (old-style)") << QStringLiteral("<form><select name=\"direction\"><option value=\"left\" selected >Left</option><option value=\"right\">Right</option></select></form>") << 0 << QMap<QString, QString> {{QStringLiteral("direction"), QStringLiteral("left")}}; QTest::newRow("Form with select/option (modern)") << QStringLiteral("<form><select name=\"direction\"><option value=\"left\" >Left</option><option selected=\"selected\" value=\"right\">Right</option></select></form>") << 0 << QMap<QString, QString> {{QStringLiteral("direction"), QStringLiteral("right")}}; } void KBibTeXNetworkingTest::onlineSearchAbstractFormParameters() { QFETCH(QString, htmlCode); QFETCH(int, startPos); QFETCH(FormData, expectedResult); OnlineSearchDummy onlineSearch(this); const FormData computedResult = onlineSearch.formParameters_public(htmlCode, startPos); QCOMPARE(expectedResult.size(), computedResult.size()); const QList<QString> keys = expectedResult.keys(); for (const QString &key : keys) { QCOMPARE(computedResult.contains(key), true); const QList<QString> expectedValues = expectedResult.values(key); const QList<QString> computedValues = computedResult.values(key); QCOMPARE(expectedValues.size(), computedValues.size()); for (int p = expectedValues.size() - 1; p >= 0; --p) { const QString &expectedValue = expectedValues[p]; const QString &computedValue = computedValues[p]; QCOMPARE(expectedValue, computedValue); } } } void KBibTeXNetworkingTest::onlineSearchAbstractSanitizeEntry_data() { QTest::addColumn<Entry *>("badInputEntry"); QTest::addColumn<Entry *>("goodOutputEntry"); QTest::newRow("Entry with type and id but without values") << new Entry(Entry::etArticle, QStringLiteral("abc123")) << new Entry(Entry::etArticle, QStringLiteral("abc123")); const Value doiValue = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("10.1000/182"))); const Value authorValue = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("Jane Doe"))); Entry *entryA1 = new Entry(Entry::etBook, QStringLiteral("abcdef")); Entry *entryA2 = new Entry(Entry::etBook, QStringLiteral("abcdef")); Value valueA1 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("http://dx.example.org/10.1000/182"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://www.kde.org"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://dx.doi.org/10.1000/183"))); entryA1->insert(Entry::ftUrl, valueA1); Value valueA2 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("10.1000/182"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("10.1000/183"))); entryA2->insert(Entry::ftDOI, valueA2); Value valueA3 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://www.kde.org"))); entryA2->insert(Entry::ftUrl, valueA3); QTest::newRow("Entry with DOI number in URL") << entryA1 << entryA2; Entry *entryB1 = new Entry(Entry::etPhDThesis, QStringLiteral("abCDef2")); Entry *entryB2 = new Entry(Entry::etPhDThesis, QStringLiteral("abCDef2")); Value valueB1 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("http://dx.example.org/10.1000/182"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://www.kde.org"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://dx.doi.org/10.1000/183"))); entryB1->insert(Entry::ftUrl, valueB1); Value valueB2 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("10.1000/182"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("10.1000/183"))); entryB1->insert(Entry::ftDOI, valueB2); entryB2->insert(Entry::ftDOI, valueB2); Value valueB3 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("https://www.kde.org"))); entryB2->insert(Entry::ftUrl, valueB3); QTest::newRow("Entry both with DOI and DOI number in URL") << entryB1 << entryB2; Entry *entryC1 = new Entry(Entry::etInProceedings, QStringLiteral("abc567")); Entry *entryC2 = new Entry(Entry::etInProceedings, QStringLiteral("abc567")); Value valueC1 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("42"))); static const QString ftIssue = QStringLiteral("issue"); entryC1->insert(ftIssue, valueC1); entryC1->insert(Entry::ftDOI, doiValue); Value valueC2 = valueC1; entryC2->insert(Entry::ftDOI, doiValue); entryC2->insert(Entry::ftNumber, valueC2); QTest::newRow("Entry with 'issue' becomes 'number'") << entryC1 << entryC2; Entry *entryD1 = new Entry(Entry::etTechReport, QStringLiteral("TR10.1000/182")); Entry *entryD2 = new Entry(Entry::etTechReport, QStringLiteral("TR10.1000/182")); entryD1->insert(Entry::ftAuthor, authorValue); entryD2->insert(Entry::ftDOI, doiValue); entryD2->insert(Entry::ftAuthor, authorValue); QTest::newRow("Entry's id contains DOI, set DOI field accordingly") << entryD1 << entryD2; Entry *entryE1 = new Entry(Entry::etMastersThesis, QStringLiteral("xyz987")); Entry *entryE2 = new Entry(Entry::etMastersThesis, QStringLiteral("xyz987")); Value valueE1 = Value() << QSharedPointer<ValueItem>(new MacroKey(QStringLiteral("TOBEREMOVED"))); entryE1->insert(Entry::ftCrossRef, valueE1); entryE1->insert(Entry::ftAuthor, authorValue); entryE2->insert(Entry::ftAuthor, authorValue); QTest::newRow("Removing 'crossref' field from Entry") << entryE1 << entryE2; Entry *entryF1 = new Entry(Entry::etInProceedings, QStringLiteral("abc567")); Entry *entryF2 = new Entry(Entry::etInProceedings, QStringLiteral("abc567")); Value valueF1 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("Bla blubber"))); static const QString ftDescription = QStringLiteral("description"); entryF1->insert(ftDescription, valueF1); entryF1->insert(Entry::ftDOI, doiValue); entryF2->insert(Entry::ftDOI, doiValue); entryF2->insert(Entry::ftAbstract, valueF1); QTest::newRow("Entry with 'description' becomes 'abstract'") << entryF1 << entryF2; Entry *entryG1 = new Entry(Entry::etPhDThesis, QStringLiteral("qwertz")); Entry *entryG2 = new Entry(Entry::etPhDThesis, QStringLiteral("qwertz")); Value valueG1 = Value() << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("September"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("/"))) << QSharedPointer<ValueItem>(new MacroKey(QStringLiteral("nov"))); entryG1->insert(Entry::ftMonth, valueG1); entryG1->insert(Entry::ftDOI, doiValue); Value valueG2 = Value() << QSharedPointer<ValueItem>(new MacroKey(QStringLiteral("sep"))) << QSharedPointer<ValueItem>(new PlainText(QStringLiteral("/"))) << QSharedPointer<ValueItem>(new MacroKey(QStringLiteral("nov"))); entryG2->insert(Entry::ftDOI, doiValue); entryG2->insert(Entry::ftMonth, valueG2); QTest::newRow("Entry with month 'September' becomes macro key 'sep'") << entryG1 << entryG2; } void KBibTeXNetworkingTest::onlineSearchAbstractSanitizeEntry() { QFETCH(Entry *, badInputEntry); QFETCH(Entry *, goodOutputEntry); QSharedPointer<Entry> badInputEntrySharedPointer(badInputEntry); OnlineSearchDummy onlineSearch(this); onlineSearch.sanitizeEntry_public(badInputEntrySharedPointer); QCOMPARE(*badInputEntrySharedPointer.data(), *goodOutputEntry); delete goodOutputEntry; } void KBibTeXNetworkingTest::associatedFilescomputeAssociateURL_data() { QTest::addColumn<QUrl>("documentUrl"); QTest::addColumn<File *>("bibTeXFile"); QTest::addColumn<AssociatedFiles::PathType>("pathType"); QTest::addColumn<QString>("expectedResult"); File *bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl::fromUserInput(QStringLiteral("https://www.example.com/bibliography/all.bib"))); - QTest::newRow("Remote URL, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptRelative << QStringLiteral("../documents/paper.pdf"); + QTest::newRow("Remote URL, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Relative << QStringLiteral("../documents/paper.pdf"); bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl::fromUserInput(QStringLiteral("https://www.example.com/bibliography/all.bib"))); - QTest::newRow("Remote URL, absolute path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptAbsolute << QStringLiteral("https://www.example.com/documents/paper.pdf"); + QTest::newRow("Remote URL, absolute path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Absolute << QStringLiteral("https://www.example.com/documents/paper.pdf"); bibTeXFile = new File(); - QTest::newRow("Empty base URL, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptRelative << QStringLiteral("https://www.example.com/documents/paper.pdf"); + QTest::newRow("Empty base URL, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Relative << QStringLiteral("https://www.example.com/documents/paper.pdf"); bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl::fromUserInput(QStringLiteral("https://www.example.com/bibliography/all.bib"))); - QTest::newRow("Empty document URL, relative path requested") << QUrl() << bibTeXFile << AssociatedFiles::ptRelative << QString(); + QTest::newRow("Empty document URL, relative path requested") << QUrl() << bibTeXFile << AssociatedFiles::PathType::Relative << QString(); bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl(QStringLiteral("bibliography/all.bib"))); - QTest::newRow("Document URL and base URL are relative, relative path requested") << QUrl(QStringLiteral("documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptRelative << QStringLiteral("documents/paper.pdf"); + QTest::newRow("Document URL and base URL are relative, relative path requested") << QUrl(QStringLiteral("documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Relative << QStringLiteral("documents/paper.pdf"); bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl::fromUserInput(QStringLiteral("https://www.example.com/bibliography/all.bib"))); - QTest::newRow("Document URL and base URL have different protocols, relative path requested") << QUrl::fromUserInput(QStringLiteral("ftp://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptRelative << QStringLiteral("ftp://www.example.com/documents/paper.pdf"); + QTest::newRow("Document URL and base URL have different protocols, relative path requested") << QUrl::fromUserInput(QStringLiteral("ftp://www.example.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Relative << QStringLiteral("ftp://www.example.com/documents/paper.pdf"); bibTeXFile = new File(); bibTeXFile->setProperty(File::Url, QUrl::fromUserInput(QStringLiteral("https://www.example.com/bibliography/all.bib"))); - QTest::newRow("Document URL and base URL have different hosts, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example2.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::ptRelative << QStringLiteral("https://www.example2.com/documents/paper.pdf"); + QTest::newRow("Document URL and base URL have different hosts, relative path requested") << QUrl::fromUserInput(QStringLiteral("https://www.example2.com/documents/paper.pdf")) << bibTeXFile << AssociatedFiles::PathType::Relative << QStringLiteral("https://www.example2.com/documents/paper.pdf"); } void KBibTeXNetworkingTest::associatedFilescomputeAssociateURL() { QFETCH(QUrl, documentUrl); QFETCH(File *, bibTeXFile); QFETCH(AssociatedFiles::PathType, pathType); QFETCH(QString, expectedResult); const QString computedResult = AssociatedFiles::computeAssociateUrl(documentUrl, bibTeXFile, pathType); QCOMPARE(expectedResult, computedResult); delete bibTeXFile; } void KBibTeXNetworkingTest::initTestCase() { // TODO } QTEST_MAIN(KBibTeXNetworkingTest) #include "kbibtexnetworkingtest.moc" diff --git a/src/test/kbibtextest.cpp b/src/test/kbibtextest.cpp index 627640b5..0322997d 100644 --- a/src/test/kbibtextest.cpp +++ b/src/test/kbibtextest.cpp @@ -1,269 +1,269 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "kbibtextest.h" #include <QProgressBar> #include <QTimer> #include <QLayout> #include <QMenu> #include <QIcon> #include <QPalette> #include <QPushButton> #include <QAction> #include <QListWidget> #include <QApplication> #include <KAboutData> #include <onlinesearch/OnlineSearchAcmPortal> #include <onlinesearch/OnlineSearchArXiv> #include <onlinesearch/OnlineSearchBibsonomy> #include <onlinesearch/OnlineSearchGoogleScholar> #include <onlinesearch/OnlineSearchCERNDS> #include <onlinesearch/OnlineSearchIEEEXplore> #include <onlinesearch/OnlineSearchIngentaConnect> #include <onlinesearch/OnlineSearchInspireHep> #include <onlinesearch/OnlineSearchIDEASRePEc> #include <onlinesearch/OnlineSearchJStor> #include <onlinesearch/OnlineSearchMathSciNet> #include <onlinesearch/OnlineSearchMRLookup> #include <onlinesearch/OnlineSearchPubMed> #include <onlinesearch/OnlineSearchScienceDirect> #include <onlinesearch/OnlineSearchSpringerLink> #include <onlinesearch/OnlineSearchSOANASAADS> #include <onlinesearch/OnlineSearchBioRxiv> #include <onlinesearch/OnlineSearchSemanticScholar> static QColor blendColors(const QColor &color1, const QColor &color2, const qreal ratio) { const int r = static_cast<int>(color1.red() * (1 - ratio) + color2.red() * ratio); const int g = static_cast<int>(color1.green() * (1 - ratio) + color2.green() * ratio); const int b = static_cast<int>(color1.blue() * (1 - ratio) + color2.blue() * ratio); return QColor(r, g, b, 255); } class TestWidget : public QWidget { Q_OBJECT private: KBibTeXTest *m_parent; QPushButton *buttonStartTest; QProgressBar *progressBar; QAction *actionStartOnlineSearchTests; public: QListWidget *messageList; TestWidget(KBibTeXTest *parent) : QWidget(parent), m_parent(parent) { QGridLayout *layout = new QGridLayout(this); buttonStartTest = new QPushButton(QIcon::fromTheme(QStringLiteral("application-x-executable")), QStringLiteral("Start Tests"), this); layout->addWidget(buttonStartTest, 0, 0, 1, 1); progressBar = new QProgressBar(this); layout->addWidget(progressBar, 0, 1, 1, 3); progressBar->setVisible(false); messageList = new QListWidget(this); layout->addWidget(messageList, 1, 0, 4, 4); setupMenus(); } void setProgress(int pos, int total) { if (pos < 0 || total < 0) { progressBar->setVisible(false); progressBar->setMaximum(1); progressBar->setValue(0); } else { progressBar->setVisible(true); progressBar->setMaximum(total); progressBar->setValue(pos); } } void setupMenus() { QMenu *menu = new QMenu(buttonStartTest); buttonStartTest->setMenu(menu); /// ** Online Search ** actionStartOnlineSearchTests = new QAction(QStringLiteral("Online Search"), m_parent); connect(actionStartOnlineSearchTests, &QAction::triggered, m_parent, &KBibTeXTest::startOnlineSearchTests); menu->addAction(actionStartOnlineSearchTests); } void setBusy(bool isBusy) { buttonStartTest->setEnabled(!isBusy); actionStartOnlineSearchTests->setEnabled(!isBusy); } }; KBibTeXTest::KBibTeXTest(QWidget *parent) : QDialog(parent), m_running(false), m_isBusy(false) { m_onlineSearchList << new OnlineSearchAcmPortal(this); m_onlineSearchList << new OnlineSearchArXiv(this); m_onlineSearchList << new OnlineSearchBibsonomy(this); m_onlineSearchList << new OnlineSearchCERNDS(this); m_onlineSearchList << new OnlineSearchGoogleScholar(this); m_onlineSearchList << new OnlineSearchIDEASRePEc(this); m_onlineSearchList << new OnlineSearchIEEEXplore(this); m_onlineSearchList << new OnlineSearchIngentaConnect(this); m_onlineSearchList << new OnlineSearchInspireHep(this); m_onlineSearchList << new OnlineSearchJStor(this); m_onlineSearchList << new OnlineSearchMathSciNet(this); m_onlineSearchList << new OnlineSearchMRLookup(this); m_onlineSearchList << new OnlineSearchPubMed(this); m_onlineSearchList << new OnlineSearchScienceDirect(this); m_onlineSearchList << new OnlineSearchSOANASAADS(this); m_onlineSearchList << new OnlineSearchSpringerLink(this); m_onlineSearchList << new OnlineSearchBioRxiv(this); m_onlineSearchList << new OnlineSearchSemanticScholar(this); m_currentOnlineSearch = m_onlineSearchList.constBegin(); setWindowTitle(QStringLiteral("KBibTeX Test Suite")); m_testWidget = new TestWidget(this); #if QT_VERSION >= 0x050b00 const int fontSize = m_testWidget->fontMetrics().horizontalAdvance(QLatin1Char('a')); #else // QT_VERSION >= 0x050b00 const int fontSize = m_testWidget->fontMetrics().width(QLatin1Char('a')); #endif // QT_VERSION >= 0x050b00 m_testWidget->setMinimumSize(fontSize * 96, fontSize * 48); QBoxLayout *boxLayout = new QVBoxLayout(this); boxLayout->addWidget(m_testWidget); connect(this, &KBibTeXTest::rejected, this, &KBibTeXTest::aboutToQuit); - addMessage(QString(QStringLiteral("Compiled for %1")).arg(KAboutData::applicationData().version()), statusInfo); + addMessage(QString(QStringLiteral("Compiled for %1")).arg(KAboutData::applicationData().version()), MessageStatus::Info); } void KBibTeXTest::addMessage(const QString &message, const MessageStatus messageStatus) { static const QIcon iconINFO = QIcon::fromTheme(QStringLiteral("dialog-information")); static const QIcon iconOK = QIcon::fromTheme(QStringLiteral("dialog-ok-apply")); static const QIcon iconERROR = QIcon::fromTheme(QStringLiteral("dialog-cancel")); static const QIcon iconAUTH = QIcon::fromTheme(QStringLiteral("dialog-cancel")); // FIXME "dialog-cancel" should be overlay on "dialog-password" static const QIcon iconNETWORK = QIcon::fromTheme(QStringLiteral("dialog-cancel")); // FIXME "dialog-cancel" should be overlay on "network-wired" QIcon icon; switch (messageStatus) { - case statusInfo: icon = iconINFO; break; - case statusOk: icon = iconOK; break; - case statusError: icon = iconERROR; break; - case statusAuth: icon = iconAUTH; break; - case statusNetwork: icon = iconNETWORK; break; + case MessageStatus::Info: icon = iconINFO; break; + case MessageStatus::Ok: icon = iconOK; break; + case MessageStatus::Error: icon = iconERROR; break; + case MessageStatus::Auth: icon = iconAUTH; break; + case MessageStatus::Network: icon = iconNETWORK; break; } QListWidgetItem *item = icon.isNull() ? new QListWidgetItem(message) : new QListWidgetItem(icon, message); item->setToolTip(item->text()); const QColor originalBgColor = QGuiApplication::palette().color(QPalette::Base); switch (messageStatus) { - case statusInfo: break; ///< nothing to do - case statusOk: item->setBackground(QBrush(blendColors(originalBgColor, Qt::green, .1))); break; - case statusError: item->setBackground(QBrush(blendColors(originalBgColor, Qt::red, .1))); break; - case statusAuth: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; - case statusNetwork: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; + case MessageStatus::Info: break; ///< nothing to do + case MessageStatus::Ok: item->setBackground(QBrush(blendColors(originalBgColor, Qt::green, .1))); break; + case MessageStatus::Error: item->setBackground(QBrush(blendColors(originalBgColor, Qt::red, .1))); break; + case MessageStatus::Auth: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; + case MessageStatus::Network: item->setBackground(QBrush(blendColors(originalBgColor, Qt::yellow, .1))); break; } m_testWidget->messageList->addItem(item); m_testWidget->messageList->scrollToBottom(); qApp->processEvents(); } void KBibTeXTest::setBusy(bool isBusy) { m_testWidget->setBusy(isBusy); if (isBusy && !m_isBusy) { /// changing to busy state QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); } else if (!isBusy && m_isBusy) { /// changing to idle state QApplication::restoreOverrideCursor(); } m_isBusy = isBusy; } void KBibTeXTest::aboutToQuit() { m_running = false; QTimer::singleShot(500, qApp, &QApplication::quit); } void KBibTeXTest::startOnlineSearchTests() { m_running = true; setBusy(true); m_testWidget->messageList->clear(); qApp->processEvents(); processNextSearch(); } void KBibTeXTest::onlineSearchStoppedSearch(int searchResult) { if (searchResult == OnlineSearchAbstract::resultNoError) { if (m_currentOnlineSearchNumFoundEntries == 0) - addMessage(QString(QStringLiteral("Got no error message searching '%1', but found NO entries")).arg((*m_currentOnlineSearch)->label()), statusError); + addMessage(QString(QStringLiteral("Got no error message searching '%1', but found NO entries")).arg((*m_currentOnlineSearch)->label()), MessageStatus::Error); else - addMessage(QString(QStringLiteral("No error searching '%1', found %2 entries")).arg((*m_currentOnlineSearch)->label()).arg(m_currentOnlineSearchNumFoundEntries), statusOk); + addMessage(QString(QStringLiteral("No error searching '%1', found %2 entries")).arg((*m_currentOnlineSearch)->label()).arg(m_currentOnlineSearchNumFoundEntries), MessageStatus::Ok); } else if (searchResult == OnlineSearchAbstract::resultAuthorizationRequired) { - addMessage(QString(QStringLiteral("Authorization required for '%1'")).arg((*m_currentOnlineSearch)->label()), statusAuth); + addMessage(QString(QStringLiteral("Authorization required for '%1'")).arg((*m_currentOnlineSearch)->label()), MessageStatus::Auth); } else if (searchResult == OnlineSearchAbstract::resultNetworkError) { - addMessage(QString(QStringLiteral("Network error for '%1'")).arg((*m_currentOnlineSearch)->label()), statusNetwork); + addMessage(QString(QStringLiteral("Network error for '%1'")).arg((*m_currentOnlineSearch)->label()), MessageStatus::Network); } else { - addMessage(QString(QStringLiteral("Error searching '%1'")).arg((*m_currentOnlineSearch)->label()), statusError); + addMessage(QString(QStringLiteral("Error searching '%1'")).arg((*m_currentOnlineSearch)->label()), MessageStatus::Error); } m_currentOnlineSearch++; progress(-1, -1); processNextSearch(); } void KBibTeXTest::progress(int pos, int total) { m_testWidget->setProgress(pos, total); } void KBibTeXTest::processNextSearch() { if (m_running && m_currentOnlineSearch != m_onlineSearchList.constEnd()) { setBusy(true); m_currentOnlineSearchNumFoundEntries = 0; - addMessage(QString(QStringLiteral("Searching '%1'")).arg((*m_currentOnlineSearch)->label()), statusInfo); + addMessage(QString(QStringLiteral("Searching '%1'")).arg((*m_currentOnlineSearch)->label()), MessageStatus::Info); QMap<QString, QString> query; query.insert(OnlineSearchAbstract::queryKeyAuthor, QStringLiteral("smith")); connect(*m_currentOnlineSearch, &OnlineSearchAbstract::stoppedSearch, this, &KBibTeXTest::onlineSearchStoppedSearch); connect(*m_currentOnlineSearch, &OnlineSearchAbstract::foundEntry, this, [this]() { ++m_currentOnlineSearchNumFoundEntries; }); connect(*m_currentOnlineSearch, &OnlineSearchAbstract::progress, this, &KBibTeXTest::progress); (*m_currentOnlineSearch)->startSearch(query, 3); } else { - addMessage(QStringLiteral("Done testing"), statusInfo); + addMessage(QStringLiteral("Done testing"), MessageStatus::Info); setBusy(false); m_running = false; QTimer::singleShot(500, this, [this]() { m_testWidget->setProgress(-1, -1); }); } } #include "kbibtextest.moc" diff --git a/src/test/kbibtextest.h b/src/test/kbibtextest.h index 6ca6d094..4cead09b 100644 --- a/src/test/kbibtextest.h +++ b/src/test/kbibtextest.h @@ -1,60 +1,60 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * 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) any later version. * * * * 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 <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEXTEST_H #define KBIBTEXTEST_H #include <QList> #include <QDialog> #include <QIcon> class OnlineSearchAbstract; class TestWidget; class KBibTeXTest : public QDialog { Q_OBJECT public: explicit KBibTeXTest(QWidget *parent = nullptr); public slots: void startOnlineSearchTests(); private slots: void aboutToQuit(); void onlineSearchStoppedSearch(int); void progress(int, int); private: - enum MessageStatus { statusInfo, statusOk, statusError, statusAuth, statusNetwork }; + enum class MessageStatus { Info, Ok, Error, Auth, Network }; bool m_running; TestWidget *m_testWidget; bool m_isBusy; QList<OnlineSearchAbstract *> m_onlineSearchList; QList<OnlineSearchAbstract *>::ConstIterator m_currentOnlineSearch; int m_currentOnlineSearchNumFoundEntries; void addMessage(const QString &message, const MessageStatus messageStatus); void setBusy(bool isBusy); void processNextSearch(); }; #endif // KBIBTEXTEST_H