diff --git a/CMakeLists.txt b/CMakeLists.txt index a4114f1..d87b5cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,51 +1,51 @@ # Lokalize project cmake_minimum_required(VERSION 3.0) # KDE Application Version, managed by release script set (RELEASE_SERVICE_VERSION_MAJOR "20") set (RELEASE_SERVICE_VERSION_MINOR "03") set (RELEASE_SERVICE_VERSION_MICRO "70") set (RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") project(lokalize VERSION ${RELEASE_SERVICE_VERSION}) -set(QT_MIN_VERSION "5.5.0") -set(KF5_MIN_VERSION "5.14.0") +set(QT_MIN_VERSION "5.11.0") +set(KF5_MIN_VERSION "5.65.0") # search packages used find_package(ECM ${KF5_MIN_VERSION} REQUIRED CONFIG) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core DBus Widgets Script Sql) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED I18n KIO XmlGui Notifications Config CoreAddons DocTools Kross Sonnet DBusAddons Crash) add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_NO_CAST_TO_ASCII) find_package(HUNSPELL) set_package_properties( HUNSPELL PROPERTIES DESCRIPTION "Library used for stemming" URL "https://hunspell.github.io/" TYPE REQUIRED PURPOSE "Required to build Lokalize.") include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) add_subdirectory(doc) add_subdirectory(src) add_subdirectory(scripts) add_subdirectory(icons) #add_subdirectory(strigi-analyzer) install(FILES org.kde.lokalize.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/org.kde.lokalize.appdata.xml b/org.kde.lokalize.appdata.xml index b9564c4..8bdfdee 100644 --- a/org.kde.lokalize.appdata.xml +++ b/org.kde.lokalize.appdata.xml @@ -1,133 +1,137 @@ org.kde.lokalize.desktop CC0-1.0 GPL-2.0+ Lokalize لوكالايز Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize ਲੋਕਾਲਾਈਜ਼ Lokalize Lokalize Lokalize Lokalize Lokalize Lokalize Локализуј Lokalizuj Локализуј Lokalizuj Lokalize Lokalize Lokalize xxLokalizexx Lokalize Lokalize Computer-aided translation system نظام ترجمة بمساعدة الحاسوب Sistema de traducción asistíu per ordenador Sistema de traducció assistida per ordinador Sistema de traducció assistida per ordinador Systém pro překlad pomocí počítače Computerassisteret oversættelsessystem (CAT) Computergestütztes Übersetzungssystem Σύστημα μετάφρασης υποβοηθούμενο από τον υπολογιστή Computer-aided translation system Sistema de traducción asistido por computador Tõlkemäluga tõlkimisrakendus Ordenagailu-bidezko itzulpen sistema Tietokoneavusteinen käännösjärjestelmä Outil de traduction assistée par ordinateur Sistema de tradución asistida por computador Sistem terjemahan berbantuan komputer Sistema di traduzione assistita 컴퓨터 보조 번역 시스템 Kompiuterizuota vertimo sistema Computerondersteund vertaalsysteem Dataassistert omsetjing ਕੰਪਿਊਟਰ-ਸਹਾਇਕ ਅਨੁਵਾਦ ਸਿਸਟਮ System tłumaczeń wspomagany komputerowo Sistema de traduções auxiliado por computador Sistema de tradução auxiliado pelo computador Система автоматизированного перевода Počítačovo riadený prekladový systém Računalniško podprt prevajalni sistem Систем за превођење помоћу рачунара Sistem za prevođenje pomoću računara Систем за превођење помоћу рачунара Sistem za prevođenje pomoću računara Datorstött översättningssystem Bilgisayar destekli çeviri sistemi Комп’ютерна система допомоги у перекладі xxComputer-aided translation systemxx 计算机辅助翻译系统 電腦協助翻譯系統

Lokalize is the localization tool for KDE software and other free and open source software. It is also a general computer-aided translation system (CAT) with which you can translate OpenDocument files (*.odt). Translate-Toolkit is used internally to extract text for translation from .odt to .xliff files and to merge translation back into .odt file.

«لوكالايز» أداة توطين لبرمجيّات «كدي» والبرمجيّات مالفتوحة المصدر والحرّة الأخرى. يمكن اعتباره أيضًا نظام ترجمة بمساعدة الحاسوب (CAT) يمكن استخدامه لترجمة ملفّات «المستندالمفتوح» (‎*.odt). تُستخدم «عُدّة التّرجمة» داخليًّا لاستخراج النّصوص لترجمتها من ملفّ ”‎.odt“ إلى ملفّات ”‎.xliff“ وأيضًا لدمج التّرجمة مرّة أخرى بملفّ ”‎.odt“.

Lokalize ye la ferramienta de traducción pa los programes de KDE y software llibre en xeneral. Tamién ye un sistema de traducción asistíu perordenador (CAT) col que pues traducir ficheros d'OpenDocument (*.odt). Translate-toolkit úsase internamente pa estrayer el testu de ficheros .odt y convertilu a xliff amás de mecer los cambeos nel ficheru .odt orixinal.

El Lokalize és l'eina de traducció pel programari de KDE i altre programari de codi lliure i obert. També és un sistema de traducció assistit per ordinador (CAT) general que amb el que es poden traduir fitxers OpenDocument (*.odt). Internament s'utilitza el Translate-Toolkit per extreure text per a la traducció des de fitxers .odt a .xliff i per tornar a fusionar la traducció al fitxer .odt.

El Lokalize és l'eina de traducció pel programari de KDE i altre programari de codi lliure i obert. També és un sistema de traducció assistit per ordinador (CAT) general que amb el que es poden traduir fitxers OpenDocument (*.odt). Internament s'utilitza el Translate-Toolkit per extreure text per a la traducció des de fitxers .odt a .xliff i per tornar a fusionar la traducció al fitxer .odt.

Lokalize ist ein Lokalisierungs-Programm für KDE-Programme und andere freie und quelloffene Software. Es ist außerdem ein rechnergestütztes Übersetzungssystem (CAT), mit dem Sie auch OpenDocument-Dateien (odt) übersetzen können. Intern wird „Translate-Toolkit“ zum Extrahieren von Text für die Übersetzung aus „odt“- in „xliff“-Dateien und für das Einfügen der Übersetzung zurück in „odt“-Dateien verwendet.

Το Lokalize είναι το εργαλείο μεταφράσεων για το λογισμικό KDE και για άλλα ελεύθερα και ανοικτού κώδικα. Είναι επίσης ένα γενικό σύστημα μετάφρασης με τη βοήθεια υπολογιστή (CAT), με την οποία μπορείτε να μεταφράσετε OpenDocument αρχεία (* .odt). Η εργαλειοθήκη-μεταφράσεων χρησιμοποιείται εσωτερικά για να εξαγάγετε το κείμενο για μετάφραση από .odt σε .xliff αρχεία και να συγχωνεύσει τη μετάφραση πίσω στο .odt αρχείο.

Lokalize is the localization tool for KDE software and other free and open source software. It is also a general computer-aided translation system (CAT) with which you can translate OpenDocument files (*.odt). Translate-Toolkit is used internally to extract text for translation from .odt to .xliff files and to merge translation back into .odt file.

Lokalize es la herramienta de traducción para el software de KDE y para otros proyectos de software libres y de código abierto. También es un sistema de traducción asistido por computador (CAT) de uso general con el que puede traducir archivos OpenDocument (*.odt). «Translate-Toolkit» se usa internamente para extraer el texto para la traducción de archivos .odt en .xliff y para volver a fusionar de nuevo las traducciones en los archivos .odt.

Lokalize on KDE ja muu avatud lähtekoodiga tarkvara tõlkimisrakendus. Samuti on see üldisemas tähenduses tõlkemäluga süsteem (CAT), mille abil saab tõlkida OpenDocument-faile (*.odt). Teksti eraldamiseks tõlkimise tarbeks .odt-failist .xliff-faili ja tõlke tagasiliitmiseks .odt-faili kasutatakse sisemiselt programmi Translate-Toolkit.

Lokalize da KDE softwarearen lokalizazio tresna, eta beste software aske eta iturburu irekidunena. Ordenagailu-bidezko itzulpen sistema orokorra (CAT) ere bada, OpenDocument fitxategiak (*.odt) itzultzeko erabili dezakezu. Barruan Translate-Toolkit erabiltzen da itzuli beharreko testua .odt-tik .xliff fitxategietara erauzteko eta itzulpenak atzera .odt fitxategietara bateratzeko.

Lokalize on KDE- ja muiden avoimen koodin ohjelmien kotoistustyökalu. Se on myös yleiskäyttöinen tietokoneavusteinen käännösjärjestelmä (CAT) OpenDocument-tiedostojen (*.odt) kääntämiseen. Lokalize poimii käännettävän tekstin .odt-tiedostosta .xliff-tiedostoihin ja yhdistää käännöksen takaisin .odt-tiedostoon sisäisesti Translate Toolkitilla.

Lokalize est un outil de localisation pour les logiciels KDE et d'autres outils libres. Il s'agit également d'un système d'aide à la traduction assisté par ordinateur pour les fichiers au format OpenDocument (*.odt). Translate-Toolkit est utilisé en interne pour extraire le texte à traduire depuis le fichier .odt vers un fichier au format .xliff puis fusionner en retour les traductions dans le fichier .odt.

Lokalize é a ferramenta de localización para software de KDE e doutros programas libres. Tamén é un sistema xeral de tradución asistida por computador (CAT polas súas siglas en inglés) que lle permite traducir ficheiros en formato OpenDocument (.odt). Internamente usa Translate-Toolkit para extraer o texto a traducir de ficheiros «.odt» e «.xliff» e para integrar as traducións de volta no ficheiro «.odt».

Lokalize adalah alat pelokalisan untuk perangkat lunak KDE dan perangkat lunak bebas open source lainnya. Yang juga sebuah sistem terjemahan berbantuan komputer (computer-aided translation 'CAT') pada umumnya sehingga Anda bisa menerjemahkan file OpenDocument (*.odt). Translate-Toolkit telah digunakan secara internal untuk mengekstrak teks untuk terjemahan dari .odt ke .xlif dan menggabungkan terjemahan kembali ke dalam file .odt.

Lokalize è lo strumento di localizzazione per il software KDE e per altri programmi liberi e a codice sorgente aperto. È, inoltre, un sistema di traduzione assistita (CAT) con il quale puoi tradurre file OpenDocument (*.odt). Translate-Toolkit è utilizzato internamente per estrarre il testo da tradurre dai file .odt a .xliff e per reinserire le traduzioni nel file .odt.

Lokalize는 KDE 및 자유 소프트웨어 지역화 도구입니다. OpenDocument 파일(*.odt)을 번역할 때 사용할 수 있는 컴퓨터 보조 번역 시스템(CAT)으로도 사용할 수 있습니다. .odt 파일에서 .xliff 파일로 번역 메시지를 추출하고 다시 .odt 파일에 번역을 적용할 때 Translate-Toolkit을 사용합니다.

Lokalize is het hulpprogramma voor lokalisatie voor KDE software en andere vrije en open-source software. Het is ook een algemeen systeem "computer-aided translation (CAT)" (vertalen met behulp van de computer) waarmee u OpenDocument bestanden (*.odt)kunt vertalen. Translate-Toolkit wordt intern gebruikt om tekst te extraheren voor vertalen van .odt naar .xliff bestanden en de vertaling terug te brengen in het .odt bestand.

Lokalize er eit omsetjingsverktøy for KDE-programvare og anna fri programvare. Det er òg eit generelt verktøy for dataassistert omsetjing (eit CAT-verktøy), som du kan bruka til å setja om OpenDocument-filer (*.odt) med. Programmet brukar Translate Toolkit internt for å gjera teksten i .odt-filene om til XLIFF-formatet og til å fletta omsetjingane frå XLIFF-filene tilbake til .odt-filene.

Lokalize jest narzędziem do tłumaczenia dla oprogramowania KDE i innego oprogramowania o darmowym i otwartym źródle. Jest to także wszechstronny system do komputerowego wspomagania tłumaczeń (CAT), dzięki któremu można tłumaczyć pliki OpenDocument (*.odt). Zestaw narzędzi do tłumaczenia jest używany wewnętrznie do wydobywania tekstu z plików .odt do plików .xliff, tłumaczenia ich, a następnie do scalania plikami .odt na wyjściu.

O Lokalize é a ferramenta de traduções do KDE e para outras aplicações de 'software' livre. Também é um sistema de traduções auxiliado por computador, com o qual poderá traduzir ficheiros em OpenDocument (*.odt). O Translate-Toolkit é usado internamente para extrair o texto a traduzir do formato .odt para ficheiros .xliff e para reunir as traduções de volta para o ficheiro .odt.

Lokalize é uma ferramenta de localização para o software KDE e outros softwares livres e de código aberto. Ele é também um sistema de tradução auxiliado pelo computador (CAT), que permite-lhe traduzir arquivos OpenDocument (*.odt). O Translate-Toolkit é usado internamente para extrair texto de arquivos .odt e transformá-lo em .xliff, de forma que possam ser traduzidos e depois mesclados ao arquivo .odt.

Lokalize — инструмент переводчика от KDE. Это также система автоматизированного перевода (CAT), позволяющая переводить документы OpenDocument (*.odt). Для извлечения текста из документа ODT в файл XLIFF и переноса перевода в обратную сторону используется Translate-Toolkit.

Lokalize je lokalizačný nástroj pre KDE a iný slobodný softvér. Je to tiež všeobecný počítačom riadený prekladový systém (CAT), ktorým môžete prekladať súbory OpenDocument (*.odt). Prekladový toolkit sa používa interne na extrakciu textu na preklad z .odt do .xliff súborov a zlúčenie prekladov naspäť do .odt súboru.

Lokalize je prevajalno orodje za programsko opremo KDE ter drugo prosto in odprtokodno programsko opremo. Je tudi splošen računalniško podprt prevajalni sistem (CAT), s katerim lahko prevajate dokumente OpenDocument (*.odt). Program uporablja Translate-Toolkit za razširitev besedila iz datoteke .odt v .xliff in nato uveljavitev sprememb nazaj v datoteko .odt.

Локализуј је локализациона алатка за КДЕ‑ов и други отворенокодни софтвер. Такође је општи систем за превођење помоћу рачунара (ЦАТ), којим можете да преводите отворенодокументске фајлове (*.odt). Интерно се користи Преводилачки прибор за извлачење текста за превођење из *.odt и *.xliff фајлова, и за стапање превода назад у *.odt фајл.

Lokalizuj je lokalizaciona alatka za KDE‑ov i drugi otvorenokodni softver. Takođe je opšti sistem za prevođenje pomoću računara (CAT), kojim možete da prevodite otvorenodokumentske fajlove (*.odt). Interno se koristi Prevodilački pribor za izvlačenje teksta za prevođenje iz *.odt i *.xliff fajlova, i za stapanje prevoda nazad u *.odt fajl.

Локализуј је локализациона алатка за КДЕ‑ов и други отворенокодни софтвер. Такође је општи систем за превођење помоћу рачунара (ЦАТ), којим можете да преводите отворенодокументске фајлове (*.odt). Интерно се користи Преводилачки прибор за извлачење текста за превођење из *.odt и *.xliff фајлова, и за стапање превода назад у *.odt фајл.

Lokalizuj je lokalizaciona alatka za KDE‑ov i drugi otvorenokodni softver. Takođe je opšti sistem za prevođenje pomoću računara (CAT), kojim možete da prevodite otvorenodokumentske fajlove (*.odt). Interno se koristi Prevodilački pribor za izvlačenje teksta za prevođenje iz *.odt i *.xliff fajlova, i za stapanje prevoda nazad u *.odt fajl.

Lokalize är översättningsverktyget för KDE-programvara och annan programvara med fri och öppen källkod. Det är också ett allmänt datorstött översättningssystem (CAT), som kan användas för att översätta OpenDocument-filer (*.odt). Translate-Toolkit används internt för att extrahera text för översättning från .odt- till .xliff-filer och för att infoga översättningen tillbaka i .odt-filen.

Lokalize, KDE ve diğer açık kaynaklı yazılım için yerelleştirme aracıdır. Ayrıca OpenDocument dosyalarını (*.odt) çevirmenizi sağlayan genel bir bilgisayar destekli çeviri sistemidir (CAT). .ODT dosyasından metni .XLIFF dosyalarına çıkarmak ve geri dönüştürmek için dahili olarak Translate-Toolkit kullanılmaktadır.

Lokalize — інструмент для локалізації програмного забезпечення KDE та іншого програмного забезпечення із відкритим кодом. Це також загальна система комп’ютеризованого перекладу (CAT), за допомогою якої ви можете перекладати файли OpenDocument (*.odt). Для видобування тексту для перекладу з файлів .odt до файлів .xliff, а також для включення перекладу назад до файла .odt використовується Translate-Toolkit.

xxLokalize is the localization tool for KDE software and other free and open source software. It is also a general computer-aided translation system (CAT) with which you can translate OpenDocument files (*.odt). Translate-Toolkit is used internally to extract text for translation from .odt to .xliff files and to merge translation back into .odt file.xx

Lokalize 是一套為 KDE 程序與其它免費與開源軟體做在地化的工具。它也是一套電腦輔助翻譯系統(Computer-Aided Translation, CAT)。您可以用這套系統翻譯OpenDocument 檔(*.odt)。內部使用 Translate-toolkit 將文字從 .odt 檔展開為 .xliff 檔,並將翻譯合併回 .odt 檔。

https://userbase.kde.org/Lokalize https://bugs.kde.org/enter_bug.cgi?format=guided&product=lokalize https://docs.kde.org/?application=lokalize https://www.kde.org/community/donations/?app=lokalize&source=appdata https://www.kde.org/images/screenshots/lokalize.png KDE lokalize + + + +
diff --git a/src/catalog/catalog.cpp b/src/catalog/catalog.cpp index a4b8cbb..9c6025a 100644 --- a/src/catalog/catalog.cpp +++ b/src/catalog/catalog.cpp @@ -1,1062 +1,1062 @@ /* **************************************************************************** This file is part of Lokalize This file contains parts of KBabel code Copyright (C) 1999-2000 by Matthias Kiefer 2001-2005 by Stanislav Visnovsky 2006 by Nicolas Goutte 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "catalog.h" #include "catalog_private.h" #include "project.h" #include "projectmodel.h" //to notify about modification #include "catalogstorage.h" #include "gettextstorage.h" #include "gettextimport.h" #include "gettextexport.h" #include "xliffstorage.h" #include "tsstorage.h" #include "mergecatalog.h" #include "version.h" #include "prefs_lokalize.h" #include "jobs.h" #include "dbfilesmodel.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //QString Catalog::supportedMimeFilters("text/x-gettext-translation application/x-xliff application/x-linguist"); //" text/x-gettext-translation-template") QString Catalog::supportedFileTypes(bool includeTemplates) { QString sep = QStringLiteral(";;"); QString all = i18n("All supported files (*.po *.pot *.xlf *.xliff *.ts)") + sep; return all + (includeTemplates ? i18n("Gettext (*.po *.pot)") : i18n("Gettext (*.po)")) + sep + i18n("XLIFF (*.xlf *.xliff)") + sep + i18n("Linguist (*.ts)"); } static const QString extensions[] = {U(".po"), U(".pot"), U(".xlf"), U(".xliff"), U(".ts")}; static const char* const xliff_states[] = { I18N_NOOP("New"), I18N_NOOP("Needs translation"), I18N_NOOP("Needs full localization"), I18N_NOOP("Needs adaptation"), I18N_NOOP("Translated"), I18N_NOOP("Needs translation review"), I18N_NOOP("Needs full localization review"), I18N_NOOP("Needs adaptation review"), I18N_NOOP("Final"), I18N_NOOP("Signed-off") }; const char* const* Catalog::states() { return xliff_states; } QStringList Catalog::supportedExtensions() { QStringList result; int i = sizeof(extensions) / sizeof(QString); while (--i >= 0) result.append(extensions[i]); return result; } bool Catalog::extIsSupported(const QString& path) { QStringList ext = supportedExtensions(); int i = ext.size(); while (--i >= 0 && !path.endsWith(ext.at(i))) ; return i != -1; } Catalog::Catalog(QObject *parent) : QUndoStack(parent) , d(this) , m_storage(0) { //cause refresh events for files modified from lokalize itself aint delivered automatically connect(this, QOverload::of(&Catalog::signalFileSaved), Project::instance()->model(), QOverload::of(&ProjectModel::slotFileSaved), Qt::QueuedConnection); QTimer* t = &(d._autoSaveTimer); t->setInterval(2 * 60 * 1000); t->setSingleShot(false); connect(t, &QTimer::timeout, this, &Catalog::doAutoSave); connect(this, QOverload<>::of(&Catalog::signalFileSaved), t, QOverload<>::of(&QTimer::start)); connect(this, QOverload<>::of(&Catalog::signalFileLoaded), t, QOverload<>::of(&QTimer::start)); connect(this, &Catalog::indexChanged, this, &Catalog::setAutoSaveDirty); connect(Project::local(), &Project::configChanged, this, &Catalog::projectConfigChanged); } Catalog::~Catalog() { clear(); //delete m_storage; //deleted in clear(); } void Catalog::clear() { setIndex(cleanIndex());//to keep TM in sync QUndoStack::clear(); d._errorIndex.clear(); d._nonApprovedIndex.clear(); d._nonApprovedNonEmptyIndex.clear(); d._emptyIndex.clear(); delete m_storage; m_storage = 0; d._filePath.clear(); d._lastModifiedPos = DocPosition(); d._modifiedEntries.clear(); while (!d._altTransCatalogs.isEmpty()) d._altTransCatalogs.takeFirst()->deleteLater(); d._altTranslations.clear(); /* d.msgidDiffList.clear(); d.msgstr2MsgidDiffList.clear(); d.diffCache.clear(); */ } void Catalog::push(QUndoCommand* cmd) { generatePhaseForCatalogIfNeeded(this); QUndoStack::push(cmd); } //BEGIN STORAGE TRANSLATION int Catalog::capabilities() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->capabilities(); } int Catalog::numberOfEntries() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->size(); } static DocPosition alterForSinglePlural(const Catalog* th, DocPosition pos) { //if source lang is english (implied) and target lang has only 1 plural form (e.g. Chinese) if (Q_UNLIKELY(th->numberOfPluralForms() == 1 && th->isPlural(pos))) pos.form = 1; return pos; } QString Catalog::msgid(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->source(alterForSinglePlural(this, pos)); } QString Catalog::msgidWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->sourceWithPlurals(pos, truncateFirstLine); } QString Catalog::msgstr(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->target(pos); } QString Catalog::msgstrWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->targetWithPlurals(pos, truncateFirstLine); } CatalogString Catalog::sourceWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->sourceWithTags(alterForSinglePlural(this, pos)); } CatalogString Catalog::targetWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->targetWithTags(pos); } CatalogString Catalog::catalogString(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->catalogString(pos.part == DocPosition::Source ? alterForSinglePlural(this, pos) : pos); } QVector Catalog::notes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->notes(pos); } QVector Catalog::developerNotes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->developerNotes(pos); } Note Catalog::setNote(const DocPosition& pos, const Note& note) { if (Q_UNLIKELY(!m_storage)) return Note(); return m_storage->setNote(pos, note); } QStringList Catalog::noteAuthors() const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->noteAuthors(); } void Catalog::attachAltTransCatalog(Catalog* altCat) { d._altTransCatalogs.append(altCat); if (numberOfEntries() != altCat->numberOfEntries()) qCWarning(LOKALIZE_LOG) << altCat->url() << "has different number of entries"; } void Catalog::attachAltTrans(int entry, const AltTrans& trans) { d._altTranslations.insert(entry, trans); } QVector Catalog::altTrans(const DocPosition& pos) const { QVector result; if (m_storage) result = m_storage->altTrans(pos); foreach (Catalog* altCat, d._altTransCatalogs) { if (pos.entry >= altCat->numberOfEntries()) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because" << pos.entry << "<" << altCat->numberOfEntries(); continue; } if (altCat->source(pos) != source(pos)) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because s don't match"; continue; } QString target = altCat->msgstr(pos); if (!target.isEmpty() && altCat->isApproved(pos)) { result << AltTrans(); result.last().target = target; result.last().type = AltTrans::Reference; result.last().origin = altCat->url(); } } if (d._altTranslations.contains(pos.entry)) result << d._altTranslations.value(pos.entry); return result; } QStringList Catalog::sourceFiles(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->sourceFiles(pos); } QString Catalog::id(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->id(pos); } QStringList Catalog::context(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->context(pos); } QString Catalog::setPhase(const DocPosition& pos, const QString& phase) { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->setPhase(pos, phase); } void Catalog::setActivePhase(const QString& phase, ProjectLocal::PersonRole role) { d._phase = phase; d._phaseRole = role; updateApprovedEmptyIndexCache(); emit activePhaseChanged(); } void Catalog::updateApprovedEmptyIndexCache() { if (Q_UNLIKELY(!m_storage)) return; //index cache TODO profile? d._nonApprovedIndex.clear(); d._nonApprovedNonEmptyIndex.clear(); d._emptyIndex.clear(); DocPosition pos(0); const int limit = m_storage->size(); while (pos.entry < limit) { if (m_storage->isEmpty(pos)) d._emptyIndex << pos.entry; if (!isApproved(pos)) { d._nonApprovedIndex << pos.entry; if (!m_storage->isEmpty(pos)) { d._nonApprovedNonEmptyIndex << pos.entry; } } ++(pos.entry); } emit signalNumberOfFuzziesChanged(); emit signalNumberOfEmptyChanged(); } QString Catalog::phase(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->phase(pos); } Phase Catalog::phase(const QString& name) const { return m_storage->phase(name); } QList Catalog::allPhases() const { return m_storage->allPhases(); } QVector Catalog::phaseNotes(const QString& phase) const { return m_storage->phaseNotes(phase); } QVector Catalog::setPhaseNotes(const QString& phase, QVector notes) { return m_storage->setPhaseNotes(phase, notes); } QMap Catalog::allTools() const { return m_storage->allTools(); } bool Catalog::isPlural(uint index) const { return m_storage && m_storage->isPlural(DocPosition(index)); } bool Catalog::isApproved(uint index) const { if (Q_UNLIKELY(!m_storage)) return false; bool extendedStates = m_storage->capabilities()&ExtendedStates; return (extendedStates &&::isApproved(state(DocPosition(index)), activePhaseRole())) || (!extendedStates && m_storage->isApproved(DocPosition(index))); } TargetState Catalog::state(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return NeedsTranslation; if (m_storage->capabilities()&ExtendedStates) return m_storage->state(pos); else return closestState(m_storage->isApproved(pos), activePhaseRole()); } bool Catalog::isEmpty(uint index) const { return m_storage && m_storage->isEmpty(DocPosition(index)); } bool Catalog::isEmpty(const DocPosition& pos) const { return m_storage && m_storage->isEmpty(pos); } bool Catalog::isEquivTrans(const DocPosition& pos) const { return m_storage && m_storage->isEquivTrans(pos); } int Catalog::binUnitsCount() const { return m_storage ? m_storage->binUnitsCount() : 0; } int Catalog::unitById(const QString& id) const { return m_storage ? m_storage->unitById(id) : 0; } QString Catalog::mimetype() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->mimetype(); } QString Catalog::fileType() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->fileType(); } CatalogType Catalog::type() { if (Q_UNLIKELY(!m_storage)) return Gettext; return m_storage->type(); } QString Catalog::sourceLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->sourceLangCode(); } QString Catalog::targetLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->targetLangCode(); } void Catalog::setTargetLangCode(const QString& targetLangCode) { if (Q_UNLIKELY(!m_storage)) return; bool notify = m_storage->targetLangCode() != targetLangCode; m_storage->setTargetLangCode(targetLangCode); if (notify) emit signalFileLoaded(); } //END STORAGE TRANSLATION //BEGIN OPEN/SAVE #define DOESNTEXIST -1 #define ISNTREADABLE -2 #define UNKNOWNFORMAT -3 KAutoSaveFile* Catalog::checkAutoSave(const QString& url) { KAutoSaveFile* autoSave = 0; QList staleFiles = KAutoSaveFile::staleFiles(QUrl::fromLocalFile(url)); foreach (KAutoSaveFile *stale, staleFiles) { if (stale->open(QIODevice::ReadOnly) && !autoSave) { autoSave = stale; autoSave->setParent(this); } else stale->deleteLater(); } if (autoSave) qCInfo(LOKALIZE_LOG) << "autoSave" << autoSave->fileName(); return autoSave; } int Catalog::loadFromUrl(const QString& filePath, const QString& saidUrl, int* fileSize, bool fast) { QFileInfo info(filePath); if (Q_UNLIKELY(!info.exists() || info.isDir())) return DOESNTEXIST; if (Q_UNLIKELY(!info.isReadable())) return ISNTREADABLE; bool readOnly = !info.isWritable(); - QTime a; a.start(); + QElapsedTimer a; a.start(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) return ISNTREADABLE;//TODO CatalogStorage* storage = nullptr; if (filePath.endsWith(QLatin1String(".po")) || filePath.endsWith(QLatin1String(".pot"))) storage = new GettextCatalog::GettextStorage; else if (filePath.endsWith(QLatin1String(".xlf")) || filePath.endsWith(QLatin1String(".xliff"))) storage = new XliffStorage; else if (filePath.endsWith(QLatin1String(".ts"))) storage = new TsStorage; else { //try harder QTextStream in(&file); int i = 0; bool gettext = false; while (!in.atEnd() && ++i < 64 && !gettext) gettext = in.readLine().contains(QLatin1String("msgid")); if (gettext) storage = new GettextCatalog::GettextStorage; else return UNKNOWNFORMAT; } int line = storage->load(&file); file.close(); if (Q_UNLIKELY(line != 0 || (!storage->size() && (line == -1)))) { delete storage; return line; } if (a.elapsed() > 100) qCDebug(LOKALIZE_LOG) << filePath << "opened in" << a.elapsed(); //ok... clear(); //commit transaction m_storage = storage; updateApprovedEmptyIndexCache(); d._numberOfPluralForms = storage->numberOfPluralForms(); d._autoSaveDirty = true; d._readOnly = readOnly; d._filePath = saidUrl.isEmpty() ? filePath : saidUrl; //set some sane role, a real phase with a nmae will be created later with the first edit command setActivePhase(QString(), Project::local()->role()); if (!fast) { KAutoSaveFile* autoSave = checkAutoSave(d._filePath); d._autoSaveRecovered = autoSave; if (autoSave) { d._autoSave->deleteLater(); d._autoSave = autoSave; //restore 'modified' status for entries MergeCatalog* mergeCatalog = new MergeCatalog(this, this); int errorLine = mergeCatalog->loadFromUrl(autoSave->fileName()); if (Q_LIKELY(errorLine == 0)) mergeCatalog->copyToBaseCatalog(); mergeCatalog->deleteLater(); d._autoSave->close(); } else d._autoSave->setManagedFile(QUrl::fromLocalFile(d._filePath)); } if (fileSize) *fileSize = file.size(); emit signalFileLoaded(); emit signalFileLoaded(d._filePath); return 0; } bool Catalog::save() { return saveToUrl(d._filePath); } //this function is not called if QUndoStack::isClean() ! bool Catalog::saveToUrl(QString localFilePath) { if (Q_UNLIKELY(!m_storage)) return true; bool nameChanged = localFilePath.length(); if (Q_LIKELY(!nameChanged)) localFilePath = d._filePath; QString localPath = QFileInfo(localFilePath).absolutePath(); if (!QFileInfo::exists(localPath)) if (!QDir::root().mkpath(localPath)) return false; QFile file(localFilePath); if (Q_UNLIKELY(!file.open(QIODevice::WriteOnly))) //i18n("Wasn't able to open file %1",filename.ascii()); return false; bool belongsToProject = localFilePath.contains(Project::instance()->poDir()); if (Q_UNLIKELY(!m_storage->save(&file, belongsToProject))) return false; file.close(); d._autoSave->remove(); d._autoSaveRecovered = false; setClean(); //undo/redo if (nameChanged) { d._filePath = localFilePath; d._autoSave->setManagedFile(QUrl::fromLocalFile(localFilePath)); } //Settings::self()->setCurrentGroup("Bookmarks"); //Settings::self()->addItemIntList(d._filePath.url(),d._bookmarkIndex); emit signalFileSaved(); emit signalFileSaved(localFilePath); return true; /* else if (status==NO_PERMISSIONS) { if (KMessageBox::warningContinueCancel(this, i18n("You do not have permission to write to file:\n%1\n" "Do you want to save to another file or cancel?", _currentURL.prettyUrl()), i18n("Error"),KStandardGuiItem::save())==KMessageBox::Continue) return fileSaveAs(); } */ } void Catalog::doAutoSave() { if (isClean() || !(d._autoSaveDirty)) return; if (Q_UNLIKELY(!m_storage)) return; if (!d._autoSave->open(QIODevice::WriteOnly)) { emit signalFileAutoSaveFailed(d._autoSave->fileName()); return; } qCInfo(LOKALIZE_LOG) << "doAutoSave" << d._autoSave->fileName(); m_storage->save(d._autoSave); d._autoSave->close(); d._autoSaveDirty = false; } void Catalog::projectConfigChanged() { setActivePhase(activePhase(), Project::local()->role()); } QByteArray Catalog::contents() { QBuffer buf; buf.open(QIODevice::WriteOnly); m_storage->save(&buf); buf.close(); return buf.data(); } //END OPEN/SAVE /** * helper method to keep db in a good shape :) * called on * 1) entry switch * 2) automatic editing code like replace or undo/redo operation **/ static void updateDB( const QString& filePath, const QString& ctxt, const CatalogString& english, const CatalogString& newTarget, int form, bool approved, const QString& dbName //const DocPosition&,//for back tracking ) { TM::UpdateJob* j = new TM::UpdateJob(filePath, ctxt, english, newTarget, form, approved, dbName); TM::threadPool()->start(j); } //BEGIN UNDO/REDO const DocPosition& Catalog::undo() { QUndoStack::undo(); return d._lastModifiedPos; } const DocPosition& Catalog::redo() { QUndoStack::redo(); return d._lastModifiedPos; } void Catalog::flushUpdateDBBuffer() { if (!Settings::autoaddTM()) return; DocPosition pos = d._lastModifiedPos; if (pos.entry == -1 || pos.entry >= numberOfEntries()) { //nothing to flush //qCWarning(LOKALIZE_LOG)<<"nothing to flush or new file opened"; return; } QString dbName; if (Project::instance()->targetLangCode() == targetLangCode()) { dbName = Project::instance()->projectID(); } else { dbName = sourceLangCode() + '-' + targetLangCode(); qCInfo(LOKALIZE_LOG) << "updating" << dbName << "because target language of project db does not match" << Project::instance()->targetLangCode() << targetLangCode(); if (!TM::DBFilesModel::instance()->m_configurations.contains(dbName)) { TM::OpenDBJob* openDBJob = new TM::OpenDBJob(dbName, TM::Local, true); connect(openDBJob, &TM::OpenDBJob::done, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex); openDBJob->m_setParams = true; openDBJob->m_tmConfig.markup = Project::instance()->markup(); openDBJob->m_tmConfig.accel = Project::instance()->accel(); openDBJob->m_tmConfig.sourceLangCode = sourceLangCode(); openDBJob->m_tmConfig.targetLangCode = targetLangCode(); TM::DBFilesModel::instance()->openDB(openDBJob); } } int form = -1; if (isPlural(pos.entry)) form = pos.form; updateDB(url(), context(pos.entry).first(), sourceWithTags(pos), targetWithTags(pos), form, isApproved(pos.entry), dbName); d._lastModifiedPos = DocPosition(); } void Catalog::setLastModifiedPos(const DocPosition& pos) { if (pos.entry >= numberOfEntries()) //bin-units return; bool entryChanged = DocPos(d._lastModifiedPos) != DocPos(pos); if (entryChanged) flushUpdateDBBuffer(); d._lastModifiedPos = pos; } bool CatalogPrivate::addToEmptyIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos, bool alreadyEmpty) { if ((!pos.offset) && (storage->target(pos).isEmpty()) && (!alreadyEmpty)) { insertInList(_emptyIndex, pos.entry); return true; } return false; } void Catalog::targetDelete(const DocPosition& pos, int count) { if (Q_UNLIKELY(!m_storage)) return; bool alreadyEmpty = m_storage->isEmpty(pos); m_storage->targetDelete(pos, count); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); } bool CatalogPrivate::removeFromUntransIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos) { if ((!pos.offset) && (storage->isEmpty(pos))) { _emptyIndex.removeAll(pos.entry); return true; } return false; } void Catalog::targetInsert(const DocPosition& pos, const QString& arg) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsert(pos, arg); emit signalEntryModified(pos); } void Catalog::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsertTag(pos, tag); emit signalEntryModified(pos); } InlineTag Catalog::targetDeleteTag(const DocPosition& pos) { if (Q_UNLIKELY(!m_storage)) return InlineTag(); bool alreadyEmpty = m_storage->isEmpty(pos); InlineTag tag = m_storage->targetDeleteTag(pos); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); return tag; } void Catalog::setTarget(DocPosition pos, const CatalogString& s) { //TODO for case of markup present m_storage->setTarget(pos, s.string); } TargetState Catalog::setState(const DocPosition& pos, TargetState state) { bool extendedStates = m_storage && m_storage->capabilities()&ExtendedStates; bool approved =::isApproved(state, activePhaseRole()); if (Q_UNLIKELY(!m_storage || (extendedStates && m_storage->state(pos) == state) || (!extendedStates && m_storage->isApproved(pos) == approved))) return this->state(pos); TargetState prevState; if (extendedStates) { prevState = m_storage->setState(pos, state); d._statesIndex[prevState].removeAll(pos.entry); insertInList(d._statesIndex[state], pos.entry); } else { prevState = closestState(!approved, activePhaseRole()); m_storage->setApproved(pos, approved); } if (!approved) { insertInList(d._nonApprovedIndex, pos.entry); if (!m_storage->isEmpty(pos)) insertInList(d._nonApprovedNonEmptyIndex, pos.entry); } else { d._nonApprovedIndex.removeAll(pos.entry); d._nonApprovedNonEmptyIndex.removeAll(pos.entry); } emit signalNumberOfFuzziesChanged(); emit signalEntryModified(pos); return prevState; } Phase Catalog::updatePhase(const Phase& phase) { return m_storage->updatePhase(phase); } void Catalog::setEquivTrans(const DocPosition& pos, bool equivTrans) { if (m_storage) m_storage->setEquivTrans(pos, equivTrans); } bool Catalog::setModified(DocPos entry, bool modified) { if (modified) { if (d._modifiedEntries.contains(entry)) return false; d._modifiedEntries.insert(entry); } else d._modifiedEntries.remove(entry); return true; } bool Catalog::isModified(DocPos entry) const { return d._modifiedEntries.contains(entry); } bool Catalog::isModified(int entry) const { if (!isPlural(entry)) return isModified(DocPos(entry, 0)); int f = numberOfPluralForms(); while (--f >= 0) if (isModified(DocPos(entry, f))) return true; return false; } //END UNDO/REDO int findNextInList(const QLinkedList& list, int index) { int nextIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key > index)) { nextIndex = key; break; } } return nextIndex; } int findPrevInList(const QLinkedList& list, int index) { int prevIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key >= index)) break; prevIndex = key; } return prevIndex; } void insertInList(QLinkedList& list, int index) { QLinkedList::Iterator it = list.begin(); while (it != list.end() && index > *it) ++it; list.insert(it, index); } void Catalog::setBookmark(uint idx, bool set) { if (set) insertInList(d._bookmarkIndex, idx); else d._bookmarkIndex.removeAll(idx); } bool isApproved(TargetState state, ProjectLocal::PersonRole role) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state >= marginStates[role]; } bool isApproved(TargetState state) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state == marginStates[0] || state == marginStates[1] || state == marginStates[2]; } TargetState closestState(bool approved, ProjectLocal::PersonRole role) { Q_ASSERT(role != ProjectLocal::Undefined); static const TargetState approvementStates[][3] = { {NeedsTranslation, NeedsReviewTranslation, NeedsReviewTranslation}, {Translated, Final, SignedOff} }; return approvementStates[approved][role]; } bool Catalog::isObsolete(int entry) const { if (Q_UNLIKELY(!m_storage)) return false; return m_storage->isObsolete(entry); } QString Catalog::originalOdfFilePath() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->originalOdfFilePath(); } void Catalog::setOriginalOdfFilePath(const QString& odfFilePath) { if (Q_UNLIKELY(!m_storage)) return; m_storage->setOriginalOdfFilePath(odfFilePath); } diff --git a/src/catalog/gettext/gettextimport.cpp b/src/catalog/gettext/gettextimport.cpp index 6f1e13f..39c68dd 100644 --- a/src/catalog/gettext/gettextimport.cpp +++ b/src/catalog/gettext/gettextimport.cpp @@ -1,705 +1,705 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer 2001-2003 by Stanislav Visnovsky 2006 by Nicolas GOUTTE 2007 by Nick Shaforostoff 2018-2019 by Simon Depiets 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "gettextimport.h" #include "lokalize_debug.h" //#include #include #include #include #include #include #include #include #include #include "catalogitem.h" using namespace GettextCatalog; // GettextImportPlugin::GettextImportPlugin(ExtraDataSaver* extraDataSaver) // : CatalogImportPlugin() // , _extraDataSaver(extraDataSaver) GettextImportPlugin::GettextImportPlugin() : CatalogImportPlugin() , _msgidMultiline(false) , _msgstrMultiline(false) , _gettextPluralForm(false) , _testBorked(false) , _obsolete(false) , _msgctxtPresent(false) , _rxMsgCtxt(QStringLiteral("^msgctxt\\s*\".*\"$")) , _rxMsgId(QStringLiteral("^msgid\\s*\".*\"$")) , _rxMsgIdPlural(QStringLiteral("^msgid_plural\\s*\".*\"$")) , _rxMsgIdPluralBorked(QStringLiteral("^msgid_plural\\s*\"?.*\"?$")) , _rxMsgIdBorked(QStringLiteral("^msgid\\s*\"?.*\"?$")) , _rxMsgIdRemQuotes(QStringLiteral("^msgid\\s*\"")) , _rxMsgLineRemEndQuote(QStringLiteral("\"$")) , _rxMsgLineRemStartQuote(QStringLiteral("^\"")) , _rxMsgLine(QStringLiteral("^\".*\\n?\"$")) , _rxMsgLineBorked(QStringLiteral("^\"?.+\\n?\"?$")) , _rxMsgStr(QStringLiteral("^msgstr\\s*\".*\\n?\"$")) , _rxMsgStrOther(QStringLiteral("^msgstr\\s*\"?.*\\n?\"?$")) , _rxMsgStrPluralStart(QStringLiteral("^msgstr\\[0\\]\\s*\".*\\n?\"$")) , _rxMsgStrPluralStartBorked(QStringLiteral("^msgstr\\[0\\]\\s*\"?.*\\n?\"?$")) , _rxMsgStrPlural(QStringLiteral("^msgstr\\[[0-9]+\\]\\s*\".*\\n?\"$")) , _rxMsgStrPluralBorked(QStringLiteral("^msgstr\\[[0-9]\\]\\s*\"?.*\\n?\"?$")) , _rxMsgStrRemQuotes(QStringLiteral("^msgstr\\s*\"?")) // , _rxMsgId (QStringLiteral("^msgid\\s*\"?.*\"?$")) , _obsoleteStart(QStringLiteral("#~")) , _msgctxtStart(QStringLiteral("msgctxt")) { } ConversionStatus GettextImportPlugin::load(QIODevice* device) { _testBorked = false; _errorLine = 0; // find codec for file // bool hadCodec; QTextCodec* codec = codecForDevice(device/*, &hadCodec*/); QTextStream stream(device); stream.seek(0); stream.setCodec(codec); //QIODevice *dev = stream.device(); //int fileSize = dev->size(); // if somethings goes wrong with the parsing, we don't have deleted the old contents CatalogItem tempHeader; //qCDebug(LOKALIZE_LOG) << "start parsing..."; - QTime aaa; - aaa.start(); +// QTime aaa; +// aaa.start(); // first read header const ConversionStatus status = readEntry(stream); bool recoveredErrorInHeader = false; if (Q_UNLIKELY(status == RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "Recovered error in header entry"; recoveredErrorInHeader = true; } else if (Q_UNLIKELY(status != OK)) { qCWarning(LOKALIZE_LOG) << "Parse error in header entry"; return status; } bool reconstructedHeader = !_msgid.isEmpty() && !_msgid.first().isEmpty(); //qCWarning(LOKALIZE_LOG) << "HEADER MSGID: " << _msgid; //qCWarning(LOKALIZE_LOG) << "HEADER MSGSTR: " << _msgstr; if (Q_UNLIKELY(reconstructedHeader)) { // The header must have an empty msgid qCWarning(LOKALIZE_LOG) << "Header entry has non-empty msgid. Creating a temporary header! " << _msgid; tempHeader.setMsgid(QString()); QString tmp( "Content-Type: text/plain; charset=UTF-8\\n" // Unknown charset "Content-Transfer-Encoding: 8bit\\n" "Mime-Version: 1.0"); tempHeader.setMsgstr(tmp); // We keep the comment of the first entry, as it might really be a header comment (at least partially) const QString comment("# Header entry was created by Lokalize.\n#\n" + _comment); tempHeader.setComment(comment); recoveredErrorInHeader = true; } else { tempHeader.setMsgid(_msgid); tempHeader.setMsgstr(_msgstr); tempHeader.setComment(_comment); } // if(tempHeader.isFuzzy()) // { // tempHeader.removeFuzzy(); // } // check if header seems to indicate docbook content generated by xml2pot const bool docbookContent = tempHeader.msgstr().contains("application/x-xml2pot"); // now parse the rest of the file uint counter = 0; QList errorIndex; //bool recoveredError=false; bool docbookFile = false; ExtraDataSaver _extraDataSaver; ConversionStatus success = OK; while (!stream.atEnd()) { if (reconstructedHeader) reconstructedHeader = false; else success = readEntry(stream); if (Q_LIKELY(success == OK)) { if (_obsolete) _extraDataSaver(_comment); else { CatalogItem tempCatItem; tempCatItem.setPlural(_gettextPluralForm); tempCatItem.setMsgid(_msgid, _msgidMultiline); tempCatItem.setMsgstr(_msgstr, _msgstrMultiline); if (_msgctxtPresent) tempCatItem.setMsgctxt(_msgctxt); tempCatItem.setComment(_comment); // add new entry to the list of entries appendCatalogItem(tempCatItem); // check if first comment seems to indicate a docbook source file if (counter == 0) docbookFile = tempCatItem.comment().contains(".docbook"); } } else if (Q_UNLIKELY(success == RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "Recovered parse error in entry: " << counter; //recoveredError=true; errorIndex.append(counter); CatalogItem tempCatItem; tempCatItem.setPlural(_gettextPluralForm); tempCatItem.setMsgid(_msgid); tempCatItem.setMsgstr(_msgstr); if (_msgctxtPresent) tempCatItem.setMsgctxt(_msgctxt); tempCatItem.setComment(_comment); // add new entry to the list of entries appendCatalogItem(tempCatItem); } else if (success == PARSE_ERROR) { qCDebug(LOKALIZE_LOG) << "Parse error in entry: " << counter; return PARSE_ERROR; } else { qCDebug(LOKALIZE_LOG) << "Unknown success status, assumig parse error " << success; return PARSE_ERROR; } counter++; } // TODO: can we check that there is no useful entry? if (Q_UNLIKELY(!counter && !recoveredErrorInHeader)) { // Empty file? (Otherwise, there would be a try of getting an entry and the count would be 1 !) qCDebug(LOKALIZE_LOG) << " Empty file?"; return PARSE_ERROR; } //qCDebug(LOKALIZE_LOG) << " ready"; // We have successfully loaded the file (perhaps with recovered errors) // qCWarning(LOKALIZE_LOG) << " done in " << aaa.elapsed() <<_extraDataSaver->extraData.size() << endl; setGeneratedFromDocbook(docbookContent || docbookFile); setHeader(tempHeader); setCatalogExtraData(_extraDataSaver.extraData); setErrorIndex(errorIndex); setCodec(codec); //setMimeTypes( "text/x-gettext-translation" ); #if 0 if (Q_UNLIKELY(recoveredErrorInHeader)) { qCDebug(LOKALIZE_LOG) << " Returning: header error"; return RECOVERED_HEADER_ERROR; } else if (Q_UNLIKELY(recoveredError)) { qCDebug(LOKALIZE_LOG) << " Returning: recovered parse error"; return RECOVERED_PARSE_ERROR; } else #endif { //qCDebug(LOKALIZE_LOG) << " Returning: OK! :-)"; return OK; } } QTextCodec* GettextImportPlugin::codecForDevice(QIODevice* device/*, bool* hadCodec*/) { QTextStream stream(device); stream.seek(0); _errorLine = 0; stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); //this way we can QTextCodec* codec = stream.codec(); //detect UTF-16 ConversionStatus status = readEntry(stream); if (Q_UNLIKELY(status != OK && status != RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "wasn't able to read header"; return codec; } QRegExp regexp(QStringLiteral("Content-Type:\\s*\\w+/[-\\w]+;?\\s*charset\\s*=\\s*(\\S+)\\s*\\\\n")); if (regexp.indexIn(_msgstr.first()) == -1) { qCDebug(LOKALIZE_LOG) << "no charset entry found"; return codec; } const QString charset = regexp.cap(1); if (charset != QLatin1String("UTF-8")) qCDebug(LOKALIZE_LOG) << "charset:" << charset; if (charset.isEmpty()) { qCWarning(LOKALIZE_LOG) << "No charset defined! Assuming UTF-8!"; return codec; } // "CHARSET" is the default charset entry in a template (pot). // characters in a template should be either pure ascii or // at least utf8, so utf8-codec can be used for both. if (charset.contains(QLatin1String("CHARSET"))) { qCDebug(LOKALIZE_LOG) << QString("file seems to be a template: using utf-8 encoding."); return QTextCodec::codecForName("utf8");; } QTextCodec* t = 0; t = QTextCodec::codecForName(charset.toLatin1()); if (t) return t; else qCWarning(LOKALIZE_LOG) << "charset found, but no codec available, using UTF-8 instead"; return codec;//UTF-8 } ConversionStatus GettextImportPlugin::readEntry(QTextStream& stream) { ConversionStatus result = readEntryRaw(stream); const QString FROM = QStringLiteral("\\\""); const QString TO = QStringLiteral("\""); _msgstr.replaceInStrings(FROM, TO); _msgid.replaceInStrings(FROM, TO); _msgctxt.replace(FROM, TO); return result; } ConversionStatus GettextImportPlugin::readEntryRaw(QTextStream& stream) { //qCDebug(LOKALIZE_LOG) << " START"; enum {Begin, Comment, Msgctxt, Msgid, Msgstr} part = Begin; _trailingNewLines = 0; bool error = false; bool recoverableError = false; //bool seenMsgctxt=false; _msgstr.clear(); _msgstr.append(QString()); _msgid.clear(); _msgid.append(QString()); _msgctxt.clear(); _msgctxtPresent = false; _comment.clear(); _gettextPluralForm = false; _obsolete = false; QStringList::Iterator msgstrIt = _msgstr.begin(); QString line; while (!stream.atEnd()) { _errorLine++; //line=stream.readLine(); if (!_bufferedLine.isEmpty()) { line = _bufferedLine; _bufferedLine.clear(); } else line = stream.readLine(); static const QString lesslessless = QStringLiteral("<<<<<<<"); static const QString isisis = QStringLiteral("======="); static const QString moremoremore = QStringLiteral(">>>>>>>"); if (Q_UNLIKELY(line.startsWith(lesslessless) || line.startsWith(isisis) || line.startsWith(moremoremore))) { // We have found a CVS/SVN conflict marker. Abort. // (It cannot be any useful data of the PO file, as otherwise the line would start with at least a quote) qCWarning(LOKALIZE_LOG) << "CVS/SVN conflict marker found! Aborting!" << endl << line << endl; return PARSE_ERROR; } // remove whitespaces from beginning and end of line line = line.trimmed(); // remember wrapping state to save file nicely int len = line.length(); if (len) { _trailingNewLines = 0; if (_maxLineLength < len && line.at(0) != '#') _maxLineLength = len; } else ++_trailingNewLines; if (part == Begin) { // ignore trailing newlines if (!len) continue; if (line.startsWith(_obsoleteStart)) { _obsolete = true; part = Comment; _comment = line; } else if (line.startsWith('#')) { part = Comment; _comment = line; } else if (line.startsWith(_msgctxtStart) && line.contains(_rxMsgCtxt)) { part = Msgctxt; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgctxt\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgctxt = line; _msgctxtPresent = true; //seenMsgctxt=true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no comment, msgctxt or msgid found after a comment: " << line; error = true; break; } } else if (part == Comment) { if (!len && _obsolete) return OK; if (!len) continue; else if (line.startsWith(_obsoleteStart)) { _comment += ('\n' + line); _obsolete = true; } else if (line.startsWith('#')) { _comment += ('\n' + line); } else if (line.startsWith(_msgctxtStart) && line.contains(_rxMsgCtxt)) { part = Msgctxt; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgctxt\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgctxt = line; _msgctxtPresent = true; //seenMsgctxt=true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp("^msgid\\s*\"?")); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no comment or msgid found after a comment while parsing: " << _comment; error = true; break; } } else if (part == Msgctxt) { if (!len) continue; else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); // add Msgctxt line to item if (_msgctxt.isEmpty()) _msgctxt = line; else _msgctxt += ('\n' + line); _msgctxtPresent = true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no msgid found after a msgctxt while parsing: " << _msgctxt; error = true; break; } } else if (part == Msgid) { if (!len) continue; else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); QStringList::Iterator it; if (_gettextPluralForm) { it = _msgid.end(); --it; } else it = _msgid.begin(); // add Msgid line to item if (it->isEmpty()) (*it) = line; else (*it) += ('\n' + line); } else if (line.contains(_rxMsgIdPlural)) { part = Msgid; _gettextPluralForm = true; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid_plural\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgid.append(line); } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdPluralBorked))) { part = Msgid; _gettextPluralForm = true; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid_plural\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgid.append(line); if (!line.isEmpty()) recoverableError = true; } else if (!_gettextPluralForm && (line.contains(_rxMsgStr))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(_rxMsgStrRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; } else if (!_gettextPluralForm && (line.contains(_rxMsgStrOther))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(_rxMsgStrRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; if (!line.isEmpty()) recoverableError = true; } else if (_gettextPluralForm && (line.contains(_rxMsgStrPluralStart))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[0\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; } else if (Q_UNLIKELY(/*_testBorked&&*/ _gettextPluralForm && line.contains(_rxMsgStrPluralStartBorked))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[0\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; if (!line.isEmpty()) recoverableError = true; } else if (line.startsWith('#')) { // ### TODO: could this be considered recoverable? qCDebug(LOKALIZE_LOG) << "comment found after a msgid while parsing: " << _msgid.first(); error = true; break; } else if (line.startsWith(QStringLiteral("msgid"))) { qCDebug(LOKALIZE_LOG) << "Another msgid found after a msgid while parsing: " << _msgid.first(); error = true; break; } // a line of the msgid with a missing quotation mark else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgLineBorked))) { recoverableError = true; // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); QStringList::Iterator it; if (_gettextPluralForm) { it = _msgid.end(); --it; } else it = _msgid.begin(); // add Msgid line to item if (it->isEmpty()) (*it) = line; else (*it) += ('\n' + line); } else { qCDebug(LOKALIZE_LOG) << "no msgstr found after a msgid while parsing: " << _msgid.first(); error = true; break; } } else if (part == Msgstr) { if (!len) break; // another line of the msgstr else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); if (!(*msgstrIt).isEmpty()) (*msgstrIt) += '\n'; (*msgstrIt) += line; } else if (_gettextPluralForm && (line.contains(_rxMsgStrPlural))) { // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[[0-9]+\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstr.append(line); msgstrIt = _msgstr.end(); --msgstrIt; } else if (line.startsWith('#') || line.startsWith(QStringLiteral("msgid"))) { _errorLine--; _bufferedLine = line; break; } else if (Q_UNLIKELY(/*_testBorked&&*/ _gettextPluralForm && (line.contains(_rxMsgStrPluralBorked)))) { // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[[0-9]\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstr.append(line); msgstrIt = _msgstr.end(); --msgstrIt; if (!line.isEmpty()) recoverableError = true; } else if (line.startsWith(QLatin1String("msgstr"))) { qCDebug(LOKALIZE_LOG) << "Another msgstr found after a msgstr while parsing: " << line << _msgstr.last(); error = true; break; } // another line of the msgstr with a missing quotation mark else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgLineBorked))) { recoverableError = true; // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); if (!(*msgstrIt).isEmpty()) (*msgstrIt) += '\n'; (*msgstrIt) += line; } else { qCDebug(LOKALIZE_LOG) << "no msgid or comment found after a msgstr while parsing: " << _msgstr.last(); error = true; break; } } } /* if(_gettextPluralForm) { qCDebug(LOKALIZE_LOG) << "gettext plural form:\n" << "msgid:\n" << _msgid.first() << "\n" << "msgid_plural:\n" << _msgid.last() << "\n" << endl; int counter=0; for(QStringList::Iterator it = _msgstr.begin(); it != _msgstr.end(); ++it) { qCDebug(LOKALIZE_LOG) << "msgstr[" << counter << "]:\n" << (*it) << endl; counter++; } } */ //qCDebug(LOKALIZE_LOG) << " NEAR RETURN"; if (Q_UNLIKELY(error)) return PARSE_ERROR; else if (Q_UNLIKELY(recoverableError)) return RECOVERED_PARSE_ERROR; else return OK; } // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/catalog/ts/tsstorage.cpp b/src/catalog/ts/tsstorage.cpp index 36fd145..192ee82 100644 --- a/src/catalog/ts/tsstorage.cpp +++ b/src/catalog/ts/tsstorage.cpp @@ -1,554 +1,554 @@ /* Copyright 2008-2014 Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "tsstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include -#include +#include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //static const char* const noyes[]={"no","yes"}; static const QString names[] = {U("source"), U("translation"), U("oldsource"), U("translatorcomment"), U("comment"), U("name"), U("numerus")}; enum TagNames {SourceTag, TargetTag, OldSourceTag, NoteTag, DevNoteTag, NameTag, PluralTag}; static const QString attrnames[] = {U("location"), U("type"), U("obsolete")}; enum AttrNames {LocationAttr, TypeAttr, ObsoleteAttr}; static const QString attrvalues[] = {U("obsolete"), U("vanished")}; enum AttValues {ObsoleteVal, VanishedVal}; TsStorage::TsStorage() : CatalogStorage() { } int TsStorage::capabilities() const { return 0;//MultipleNotes; } //BEGIN OPEN/SAVE int TsStorage::load(QIODevice* device) { - QTime chrono; chrono.start(); + QElapsedTimer chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); if (!success) { qCWarning(LOKALIZE_LOG) << "parse error" << errorMsg << errorLine; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("sourcelanguage")); m_targetLangCode = file.attribute(QStringLiteral("language")); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("message")); qCWarning(LOKALIZE_LOG) << chrono.elapsed() << "secs, " << entries.size() << "entries"; return 0; } bool TsStorage::save(QIODevice* device, bool belongsToProject) { Q_UNUSED(belongsToProject) QTextStream stream(device); m_doc.save(stream, 4); return true; } //END OPEN/SAVE //BEGIN STORAGE TRANSLATION int TsStorage::size() const { //return m_map.size(); return entries.size(); } /** * helper structure used during XLIFF XML walk-through */ struct TsContentEditingData { enum ActionType {Get, DeleteText, InsertText, CheckLength}; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get TsContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText TsContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText TsContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} }; static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on TsContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, TsContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == TsContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == TsContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == TsContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString TsStorage::catalogString(QDomElement contentElement) const { CatalogString catalogString; TsContentEditingData data(TsContentEditingData::Get); catalogString.string = content(contentElement, &data); return catalogString; } CatalogString TsStorage::catalogString(const DocPosition& pos) const { return catalogString(pos.part == DocPosition::Target ? targetForPos(pos) : sourceForPos(pos.entry)); } CatalogString TsStorage::targetWithTags(DocPosition pos) const { return catalogString(targetForPos(pos)); } CatalogString TsStorage::sourceWithTags(DocPosition pos) const { return catalogString(sourceForPos(pos.entry)); } QString TsStorage::source(const DocPosition& pos) const { return content(sourceForPos(pos.entry)); } QString TsStorage::target(const DocPosition& pos) const { return content(targetForPos(pos)); } QString TsStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = source(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } QString TsStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = target(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } void TsStorage::targetDelete(const DocPosition& pos, int count) { TsContentEditingData data(pos.offset, count); content(targetForPos(pos), &data); } void TsStorage::targetInsert(const DocPosition& pos, const QString& arg) { qCWarning(LOKALIZE_LOG) << pos.entry << arg; QDomElement targetEl = targetForPos(pos); //BEGIN add <*target> if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(names[SourceTag]); targetEl = unitEl.insertAfter(m_doc.createElement(names[TargetTag]), refNode).toElement(); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag TsContentEditingData data(pos.offset, arg); content(targetEl, &data); } void TsStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector TsStorage::altTrans(const DocPosition& pos) const { QVector result; QString oldsource = content(unitForPos(pos.entry).firstChildElement(names[OldSourceTag])); if (!oldsource.isEmpty()) result << AltTrans(CatalogString(oldsource), i18n("Previous source value, saved by lupdate tool")); return result; } QStringList TsStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(attrnames[LocationAttr]); while (!elem.isNull()) { QString sourcefile = elem.attribute(QStringLiteral("filename")); QString linenumber = elem.attribute(QStringLiteral("line")); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile + ':' + linenumber); elem = elem.nextSiblingElement(attrnames[LocationAttr]); } //qSort(result); return result; } QVector TsStorage::notes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[NoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[NoteTag]); } return result; } QVector TsStorage::developerNotes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[DevNoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[DevNoteTag]); } return result; } Note TsStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< if needed QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); //asking directly to bypass plural state detection if (target.attribute(attrnames[TypeAttr]) == attrvalues[ObsoleteVal]) return; if (approved) target.removeAttribute(attrnames[TypeAttr]); else target.setAttribute(attrnames[TypeAttr], QStringLiteral("unfinished")); } bool TsStorage::isApproved(const DocPosition& pos) const { QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); return !target.hasAttribute(attrnames[TypeAttr]) || target.attribute(attrnames[TypeAttr]) == attrvalues[VanishedVal]; } bool TsStorage::isObsolete(int entry) const { QDomElement target = unitForPos(entry).firstChildElement(names[TargetTag]); QString v = target.attribute(attrnames[TypeAttr]); return v == attrvalues[ObsoleteVal] || v == attrvalues[VanishedVal]; } bool TsStorage::isEmpty(const DocPosition& pos) const { TsContentEditingData data(TsContentEditingData::CheckLength); return content(targetForPos(pos), &data).isEmpty(); } bool TsStorage::isEquivTrans(const DocPosition& pos) const { Q_UNUSED(pos) return true;//targetForPos(pos.entry).attribute("equiv-trans")!="no"; } void TsStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { Q_UNUSED(pos) Q_UNUSED(equivTrans) //targetForPos(pos.entry).setAttribute("equiv-trans",noyes[equivTrans]); } QDomElement TsStorage::unitForPos(int pos) const { return entries.at(pos).toElement(); } QDomElement TsStorage::targetForPos(DocPosition pos) const { QDomElement unit = unitForPos(pos.entry); QDomElement translation = unit.firstChildElement(names[TargetTag]); if (!unit.hasAttribute(names[PluralTag])) return translation; if (pos.form == -1) pos.form = 0; QDomNodeList forms = translation.elementsByTagName(QStringLiteral("numerusform")); while (pos.form >= forms.size()) translation.appendChild(unit.ownerDocument().createElement(QStringLiteral("numerusform"))); return forms.at(pos.form).toElement(); } QDomElement TsStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(names[SourceTag]); } void TsStorage::setTargetLangCode(const QString& langCode) { m_targetLangCode = langCode; QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); if (m_targetLangCode != file.attribute(QStringLiteral("language")).replace('-', '_')) { QString l = langCode; file.setAttribute(QStringLiteral("language"), l.replace('_', '-')); } } //END STORAGE TRANSLATION diff --git a/src/catalog/xliff/xliffstorage.cpp b/src/catalog/xliff/xliffstorage.cpp index 1e995c8..8de5c78 100644 --- a/src/catalog/xliff/xliffstorage.cpp +++ b/src/catalog/xliff/xliffstorage.cpp @@ -1,1034 +1,1034 @@ /* Copyright 2008-2009 Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "xliffstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include -#include +#include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif static const QString noyes[] = {U("no"), U("yes")}; static const QString bintargettarget[] = {U("bin-target"), U("target")}; static const QString binsourcesource[] = {U("bin-source"), U("source")}; static const QString NOTE = U("note"); XliffStorage::XliffStorage() : CatalogStorage() { } int XliffStorage::capabilities() const { return KeepsNoteAuthors | MultipleNotes | Phases | ExtendedStates | Tags; } //BEGIN OPEN/SAVE int XliffStorage::load(QIODevice* device) { - QTime chrono; chrono.start(); + QElapsedTimer chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); QString FILE = QStringLiteral("file"); if (!success || m_doc.elementsByTagName(FILE).isEmpty()) { qCWarning(LOKALIZE_LOG) << errorMsg; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(FILE).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("source-language")).replace(u'-', u'_'); m_targetLangCode = file.attribute(QStringLiteral("target-language")).replace(u'-', u'_'); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("trans-unit")); int size = entries.size(); m_map.clear(); m_map.reserve(size); for (int i = 0; i < size; ++i) { QDomElement parentElement = entries.at(i).parentNode().toElement(); //if (Q_UNLIKELY( e.isNull() ))//sanity // continue; m_map << i; m_unitsById[entries.at(i).toElement().attribute(QStringLiteral("id"))] = i; if (parentElement.tagName() == QLatin1String("group") && parentElement.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) { m_plurals.insert(i); int localPluralNum = m_numberOfPluralForms; while (--localPluralNum > 0 && (++i) < size) { QDomElement p = entries.at(i).parentNode().toElement(); if (p.tagName() == QLatin1String("group") && p.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) continue; parentElement.appendChild(entries.at(m_map.last()).cloneNode()); } } } binEntries = m_doc.elementsByTagName(QStringLiteral("bin-unit")); size = binEntries.size(); int offset = m_map.size(); for (int i = 0; i < size; ++i) m_unitsById[binEntries.at(i).toElement().attribute(QStringLiteral("id"))] = offset + i; // entries=m_doc.elementsByTagName("body"); // uint i=0; // uint lim=size(); // while (i msgstr(item.msgstrPlural()); // while (msgstr.count() tags; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get ContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText ContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText ContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} ///InsertTag ContentEditingData(int p, const InlineTag& range) : pos(p) , lengthOfStringToRemove(-1) , actionType(InsertTag) { tags.append(range); } ///DeleteTag ContentEditingData(int p) : pos(p) , lengthOfStringToRemove(-1) , actionType(DeleteTag) {} }; static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on ContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, ContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == ContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == ContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == ContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } //BEGIN INSERT TAG else if (data->actionType == ContentEditingData::InsertTag) { const InlineTag& tag = data->tags.first(); QString mid = cData.mid(localStartPos); qCDebug(LOKALIZE_LOG) << "inserting tag" << tag.name() << tag.id << tag.start << tag.end << mid << data->pos << startingPos; if (mid.size()) c.deleteData(localStartPos, mid.size()); QDomElement newNode = elem.insertAfter(elem.ownerDocument().createElement(tag.getElementName()), n).toElement(); newNode.setAttribute(QStringLiteral("id"), tag.id); if (!tag.xid.isEmpty()) newNode.setAttribute(QStringLiteral("xid"), tag.xid); if (tag.isPaired() && tag.end > (tag.start + 1)) { //qCWarning(LOKALIZE_LOG)<<"isPaired"; int len = tag.end - tag.start - 1; //-image symbol int localLen = qMin(len, mid.size()); if (localLen) { //appending text //qCWarning(LOKALIZE_LOG)<<"localLen. appending"<missingLen (or siblings end) int childrenCumulativeLen = 0; QDomNode sibling = newNode.nextSibling(); while (!sibling.isNull()) { //&&(childrenCumulativeLen missingLen) { if (tmp.isCharacterData()) { //divide the last string const QString& endData = tmp.toCharacterData().data(); QString last = endData.left(endData.size() - (childrenCumulativeLen - missingLen)); newNode.appendChild(elem.ownerDocument().createTextNode(last)); tmp.toCharacterData().deleteData(0, last.size()); //qCWarning(LOKALIZE_LOG)<<"end of add"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END INSERT TAG cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } else if (n.isElement()) { QDomElement el = n.toElement(); //BEGIN DELETE TAG if (data && data->actionType == ContentEditingData::DeleteTag && data->pos == startingPos) { //qCWarning(LOKALIZE_LOG)<<"start deleting tag"; data->tags.append(InlineTag(startingPos, -1, InlineTag::getElementType(el.tagName().toUtf8()), el.attribute("id"), el.attribute("xid"))); if (data->tags.first().isPaired()) { //get end position ContentEditingData subData(ContentEditingData::Get); QString subContent = doContent(el, startingPos, &subData); data->tags[0].end = 1 + startingPos + subContent.size(); //tagsymbol+text //qCWarning(LOKALIZE_LOG)<<"get end position"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END DELETE TAG if (!seenCharacterDataAfterElement) //add empty charData child so that user could add some text elem.insertBefore(elem.ownerDocument().createTextNode(QString()), n); seenCharacterDataAfterElement = false; if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } int oldStartingPos = startingPos; //detect type of the tag InlineTag::InlineElement i = InlineTag::getElementType(el.tagName().toUtf8()); //1 or 2 images to represent it? //2 = there may be content inside if (InlineTag::isPaired(i)) { QString recursiveContent = doContent(el, startingPos, data); if (!recursiveContent.isEmpty()) { result += recursiveContent; startingPos += recursiveContent.size(); } if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } } if (data && data->actionType == ContentEditingData::Get) { QString id = el.attribute(QStringLiteral("id")); if (i == InlineTag::mrk) //TODO attr map id = el.attribute(QStringLiteral("mtype")); //qCWarning(LOKALIZE_LOG)<<"tagName"<tags.append(InlineTag(oldStartingPos - 1, startingPos - 1, i, id, el.attribute(QStringLiteral("xid")))); } } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString XliffStorage::catalogString(QDomElement unit, DocPosition::Part part) const { static const QString names[] = {U("source"), U("target"), U("seg-source")}; CatalogString catalogString; ContentEditingData data(ContentEditingData::Get); int nameIndex = part == DocPosition::Target; if (nameIndex == 0 && !unit.firstChildElement(names[2]).isNull()) nameIndex = 2; catalogString.string = content(unit.firstChildElement(names[nameIndex]), &data); catalogString.tags = data.tags; return catalogString; } CatalogString XliffStorage::catalogString(const DocPosition& pos) const { return catalogString(unitForPos(pos.entry), pos.part); } CatalogString XliffStorage::targetWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Target); } CatalogString XliffStorage::sourceWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Source); } static QString genericContent(QDomElement elem, bool nonbin) { return nonbin ? content(elem) : elem.firstChildElement(QStringLiteral("external-file")).attribute(QStringLiteral("href")); } QString XliffStorage::source(const DocPosition& pos) const { return genericContent(sourceForPos(pos.entry), pos.entry < size()); } QString XliffStorage::target(const DocPosition& pos) const { return genericContent(targetForPos(pos.entry), pos.entry < size()); } QString XliffStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = source(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } QString XliffStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = target(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } void XliffStorage::targetDelete(const DocPosition& pos, int count) { if (pos.entry < size()) { ContentEditingData data(pos.offset, count); content(targetForPos(pos.entry), &data); } else { //only bulk delete requests are generated targetForPos(pos.entry).firstChildElement(QStringLiteral("external-file")).setAttribute(QStringLiteral("href"), QString()); } } void XliffStorage::targetInsert(const DocPosition& pos, const QString& arg) { //qCWarning(LOKALIZE_LOG)<<"targetinsert"< if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(QStringLiteral("seg-source")); //obey standard if (refNode.isNull()) refNode = unitEl.firstChildElement(binsourcesource[pos.entry < size()]); targetEl = unitEl.insertAfter(m_doc.createElement(bintargettarget[pos.entry < size()]), refNode).toElement(); targetEl.setAttribute(QStringLiteral("state"), QStringLiteral("new")); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag if (pos.entry >= size()) { QDomElement ef = targetEl.firstChildElement(QStringLiteral("external-file")); if (ef.isNull()) ef = targetEl.appendChild(m_doc.createElement(QStringLiteral("external-file"))).toElement(); ef.setAttribute(QStringLiteral("href"), arg); return; } ContentEditingData data(pos.offset, arg); content(targetEl, &data); } void XliffStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { targetInsert(pos, QString()); //adds if needed ContentEditingData data(tag.start, tag); content(targetForPos(pos.entry), &data); } InlineTag XliffStorage::targetDeleteTag(const DocPosition& pos) { ContentEditingData data(pos.offset); content(targetForPos(pos.entry), &data); if (data.tags[0].end == -1) data.tags[0].end = data.tags[0].start; return data.tags.first(); } void XliffStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector XliffStorage::altTrans(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("alt-trans")); while (!elem.isNull()) { AltTrans aTrans; aTrans.source = catalogString(elem, DocPosition::Source); aTrans.target = catalogString(elem, DocPosition::Target); aTrans.phase = elem.attribute(QStringLiteral("phase-name")); aTrans.origin = elem.attribute(QStringLiteral("origin")); aTrans.score = elem.attribute(QStringLiteral("match-quality")).toInt(); aTrans.lang = elem.firstChildElement(QStringLiteral("target")).attribute(QStringLiteral("xml:lang")); const char* const types[] = { "proposal", "previous-version", "rejected", "reference", "accepted" }; QString typeStr = elem.attribute(QStringLiteral("alttranstype")); int i = -1; while (++i < int(sizeof(types) / sizeof(char*)) && types[i] != typeStr) ; aTrans.type = AltTrans::Type(i); result << aTrans; elem = elem.nextSiblingElement(QStringLiteral("alt-trans")); } return result; } static QDomElement phaseElement(QDomDocument m_doc, const QString& name, QDomElement& phasegroup) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); phasegroup = header.firstChildElement(QStringLiteral("phase-group")); if (phasegroup.isNull()) { phasegroup = m_doc.createElement(QStringLiteral("phase-group")); //order following XLIFF spec QDomElement skl = header.firstChildElement(QStringLiteral("skl")); if (!skl.isNull()) header.insertAfter(phasegroup, skl); else header.insertBefore(phasegroup, header.firstChildElement()); } QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull() && phaseElem.attribute(QStringLiteral("phase-name")) != name) phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); return phaseElem; } static Phase phaseFromElement(QDomElement phaseElem) { Phase phase; phase.name = phaseElem.attribute(QStringLiteral("phase-name")); phase.process = phaseElem.attribute(QStringLiteral("process-name")); phase.company = phaseElem.attribute(QStringLiteral("company-name")); phase.contact = phaseElem.attribute(QStringLiteral("contact-name")); phase.email = phaseElem.attribute(QStringLiteral("contact-email")); phase.phone = phaseElem.attribute(QStringLiteral("contact-phone")); phase.tool = phaseElem.attribute(QStringLiteral("tool-id")); phase.date = QDate::fromString(phaseElem.attribute(QStringLiteral("date")), Qt::ISODate); return phase; } Phase XliffStorage::updatePhase(const Phase& phase) { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phase.name, phasegroup); Phase prev = phaseFromElement(phaseElem); if (phaseElem.isNull() && !phase.name.isEmpty()) { phaseElem = phasegroup.appendChild(m_doc.createElement(QStringLiteral("phase"))).toElement(); phaseElem.setAttribute(QStringLiteral("phase-name"), phase.name); } phaseElem.setAttribute(QStringLiteral("process-name"), phase.process); if (!phase.company.isEmpty()) phaseElem.setAttribute(QStringLiteral("company-name"), phase.company); phaseElem.setAttribute(QStringLiteral("contact-name"), phase.contact); phaseElem.setAttribute(QStringLiteral("contact-email"), phase.email); //Q_ASSERT(phase.contact.length()); //is empty when exiting w/o saving if (!phase.phone.isEmpty()) phaseElem.setAttribute(QLatin1String("contact-phone"), phase.phone); phaseElem.setAttribute(QStringLiteral("tool-id"), phase.tool); if (phase.date.isValid()) phaseElem.setAttribute(QStringLiteral("date"), phase.date.toString(Qt::ISODate)); return prev; } QList XliffStorage::allPhases() const { QList result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement phasegroup = header.firstChildElement(QStringLiteral("phase-group")); QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull()) { result.append(phaseFromElement(phaseElem)); phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); } return result; } Phase XliffStorage::phase(const QString& name) const { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, name, phasegroup); return phaseFromElement(phaseElem); } QMap XliffStorage::allTools() const { QMap result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement toolElem = header.firstChildElement(QStringLiteral("tool")); while (!toolElem.isNull()) { Tool tool; tool.tool = toolElem.attribute(QStringLiteral("tool-id")); tool.name = toolElem.attribute(QStringLiteral("tool-name")); tool.version = toolElem.attribute(QStringLiteral("tool-version")); tool.company = toolElem.attribute(QStringLiteral("tool-company")); result.insert(tool.tool, tool); toolElem = toolElem.nextSiblingElement(QStringLiteral("tool")); } return result; } QStringList XliffStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("context-group")); while (!elem.isNull()) { if (elem.attribute(QStringLiteral("purpose")).contains(QLatin1String("location"))) { QDomElement context = elem.firstChildElement(QStringLiteral("context")); while (!context.isNull()) { QString sourcefile; QString linenumber; const QString contextType = context.attribute(QStringLiteral("context-type")); if (contextType == QLatin1String("sourcefile")) sourcefile = context.text(); else if (contextType == QLatin1String("linenumber")) linenumber = context.text(); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile + ':' + linenumber); context = context.nextSiblingElement(QStringLiteral("context")); } } elem = elem.nextSiblingElement(QStringLiteral("context-group")); } //qSort(result); return result; } static void initNoteFromElement(Note& note, QDomElement elem) { note.content = elem.text(); note.from = elem.attribute(QStringLiteral("from")); note.lang = elem.attribute(QStringLiteral("xml:lang")); if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("source")) note.annotates = Note::Source; else if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("target")) note.annotates = Note::Target; bool ok; note.priority = elem.attribute(QStringLiteral("priority")).toInt(&ok); if (!ok) note.priority = 0; } QVector XliffStorage::notes(const DocPosition& pos) const { QList result; QDomElement elem = entries.at(m_map.at(pos.entry)).firstChildElement(NOTE); while (!elem.isNull()) { Note note; initNoteFromElement(note, elem); result.append(note); elem = elem.nextSiblingElement(NOTE); } std::sort(result.begin(), result.end()); return result.toVector(); } QVector XliffStorage::developerNotes(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QVector(); } Note XliffStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< result; QDomNodeList notes = m_doc.elementsByTagName(NOTE); int i = notes.size(); while (--i >= 0) { QString from = notes.at(i).toElement().attribute(QStringLiteral("from")); if (!from.isEmpty()) result.insert(from); } return result.values(); } QVector phaseNotes(QDomDocument m_doc, const QString& phasename, bool remove = false) { QVector result; QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); QDomElement noteElem = phaseElem.firstChildElement(NOTE); while (!noteElem.isNull()) { Note note; initNoteFromElement(note, noteElem); result.append(note); QDomElement old = noteElem; noteElem = noteElem.nextSiblingElement(NOTE); if (remove) phaseElem.removeChild(old); } return result; } QVector XliffStorage::phaseNotes(const QString& phasename) const { return ::phaseNotes(m_doc, phasename, false); } QVector XliffStorage::setPhaseNotes(const QString& phasename, QVector notes) { QVector result =::phaseNotes(m_doc, phasename, true); QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); foreach (const Note& note, notes) { QDomElement elem = phaseElem.appendChild(m_doc.createElement(NOTE)).toElement(); elem.appendChild(m_doc.createTextNode(note.content)); if (!note.from.isEmpty()) elem.setAttribute(QStringLiteral("from"), note.from); if (note.priority) elem.setAttribute(QStringLiteral("priority"), note.priority); } return result; } QString XliffStorage::setPhase(const DocPosition& pos, const QString& phase) { QString PHASENAME = QStringLiteral("phase-name"); targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); QString result = target.attribute(PHASENAME); if (phase.isEmpty()) target.removeAttribute(PHASENAME); else if (phase != result) target.setAttribute(PHASENAME, phase); return result; } QString XliffStorage::phase(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); return target.attribute(QStringLiteral("phase-name")); } QStringList XliffStorage::context(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QStringList(QString()); } QStringList XliffStorage::matchData(const DocPosition& pos) const { Q_UNUSED(pos); return QStringList(); } QString XliffStorage::id(const DocPosition& pos) const { return unitForPos(pos.entry).attribute(QStringLiteral("id")); } bool XliffStorage::isPlural(const DocPosition& pos) const { return m_plurals.contains(pos.entry); } /* bool XliffStorage::isApproved(const DocPosition& pos) const { return entries.at(m_map.at(pos.entry)).toElement().attribute("approved")=="yes"; } void XliffStorage::setApproved(const DocPosition& pos, bool approved) { static const char* const noyes[]={"no","yes"}; entries.at(m_map.at(pos.entry)).toElement().setAttribute("approved",noyes[approved]); } */ static const QString xliff_states[] = { U("new"), U("needs-translation"), U("needs-l10n"), U("needs-adaptation"), U("translated"), U("needs-review-translation"), U("needs-review-l10n"), U("needs-review-adaptation"), U("final"), U("signed-off") }; TargetState stringToState(const QString& state) { int i = sizeof(xliff_states) / sizeof(QString); while (--i > 0 && state != xliff_states[i]) ; return TargetState(i); } TargetState XliffStorage::setState(const DocPosition& pos, TargetState state) { targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); TargetState prev = stringToState(target.attribute(QStringLiteral("state"))); target.setAttribute(QStringLiteral("state"), xliff_states[state]); unitForPos(pos.entry).setAttribute(QStringLiteral("approved"), noyes[state == SignedOff]); return prev; } TargetState XliffStorage::state(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); if (!target.hasAttribute(QStringLiteral("state")) && unitForPos(pos.entry).attribute(QStringLiteral("approved")) == QLatin1String("yes")) return SignedOff; return stringToState(target.attribute(QStringLiteral("state"))); } bool XliffStorage::isEmpty(const DocPosition& pos) const { ContentEditingData data(ContentEditingData::CheckLength); return content(targetForPos(pos.entry), &data).isEmpty(); } bool XliffStorage::isEquivTrans(const DocPosition& pos) const { return targetForPos(pos.entry).attribute(QStringLiteral("equiv-trans")) != QLatin1String("no"); } void XliffStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { targetForPos(pos.entry).setAttribute(QStringLiteral("equiv-trans"), noyes[equivTrans]); } QDomElement XliffStorage::unitForPos(int pos) const { if (pos < size()) return entries.at(m_map.at(pos)).toElement(); return binEntries.at(pos - size()).toElement(); } QDomElement XliffStorage::targetForPos(int pos) const { return unitForPos(pos).firstChildElement(bintargettarget[pos < size()]); } QDomElement XliffStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(binsourcesource[pos < size()]); } int XliffStorage::binUnitsCount() const { return binEntries.size(); } int XliffStorage::unitById(const QString& id) const { return m_unitsById.contains(id) ? m_unitsById.value(id) : -1; } QString XliffStorage::originalOdfFilePath() { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.attribute(QStringLiteral("original")); } void XliffStorage::setOriginalOdfFilePath(const QString& odfFilePath) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.setAttribute(QStringLiteral("original"), odfFilePath); } //END STORAGE TRANSLATION diff --git a/src/cataloglistview/catalogmodel.cpp b/src/cataloglistview/catalogmodel.cpp index 21e4bfc..4f31640 100644 --- a/src/cataloglistview/catalogmodel.cpp +++ b/src/cataloglistview/catalogmodel.cpp @@ -1,359 +1,367 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "catalogmodel.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include #include #include #include #include #include #define DYNAMICFILTER_LIMIT 256 QVector CatalogTreeModel::m_fonts; CatalogTreeModel::CatalogTreeModel(QObject* parent, Catalog* catalog) : QAbstractItemModel(parent) , m_catalog(catalog) , m_ignoreAccel(true) { if (m_fonts.isEmpty()) { QVector fonts(4, QApplication::font()); fonts[1].setItalic(true); //fuzzy fonts[2].setBold(true); //modified fonts[3].setItalic(true); //fuzzy fonts[3].setBold(true); //modified m_fonts.reserve(4); for (int i = 0; i < 4; i++) m_fonts << fonts.at(i); } connect(catalog, &Catalog::signalEntryModified, this, &CatalogTreeModel::reflectChanges); connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &CatalogTreeModel::fileLoaded); } QModelIndex CatalogTreeModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } QModelIndex CatalogTreeModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int CatalogTreeModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return DisplayedColumnCount; } void CatalogTreeModel::fileLoaded() { beginResetModel(); endResetModel(); } void CatalogTreeModel::reflectChanges(DocPosition pos) { emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); #if 0 I disabled dynamicSortFilter function //lazy sorting/filtering if (rowCount() < DYNAMICFILTER_LIMIT || m_prevChanged != pos) { qCWarning(LOKALIZE_LOG) << "first dataChanged emitment" << pos.entry; emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); if (!(rowCount() < DYNAMICFILTER_LIMIT)) { qCWarning(LOKALIZE_LOG) << "second dataChanged emitment" << m_prevChanged.entry; emit dataChanged(index(m_prevChanged.entry, 0), index(m_prevChanged.entry, DisplayedColumnCount - 1)); } } m_prevChanged = pos; #endif } int CatalogTreeModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_catalog->numberOfEntries(); } QVariant CatalogTreeModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (static_cast(section)) { case CatalogModelColumns::Key: return i18nc("@title:column", "Entry"); case CatalogModelColumns::Source: return i18nc("@title:column Original text", "Source"); case CatalogModelColumns::Target: return i18nc("@title:column Text in target language", "Target"); case CatalogModelColumns::Notes: return i18nc("@title:column", "Notes"); case CatalogModelColumns::Context: return i18nc("@title:column", "Context"); case CatalogModelColumns::Files: return i18nc("@title:column", "Files"); case CatalogModelColumns::TranslationStatus: return i18nc("@title:column", "Translation Status"); + case CatalogModelColumns::SourceLength: + return i18nc("@title:column Length of the original text", "Source length"); + case CatalogModelColumns::TargetLength: + return i18nc("@title:column Length of the text in target language", "Target length"); default: return {}; } } QVariant CatalogTreeModel::data(const QModelIndex& index, int role) const { if (m_catalog->numberOfEntries() <= index.row()) return QVariant(); const CatalogModelColumns column = static_cast(index.column()); if (role == Qt::SizeHintRole) { //no need to cache because of uniform row heights return QFontMetrics(QApplication::font()).size(Qt::TextSingleLine, QString::fromLatin1(" ")); } else if (role == Qt::FontRole/* && index.column()==Target*/) { bool fuzzy = !m_catalog->isApproved(index.row()); bool modified = m_catalog->isModified(index.row()); return m_fonts.at(fuzzy * 1 | modified * 2); } else if (role == Qt::ForegroundRole) { if (m_catalog->isBookmarked(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::LinkText); } if (m_catalog->isObsolete(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::InactiveText); } } else if (role == Qt::ToolTipRole) { if (column != CatalogModelColumns::TranslationStatus) { return {}; } switch (getTranslationStatus(index.row())) { case TranslationStatus::Ready: return i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"); case TranslationStatus::NeedsReview: return i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"); case TranslationStatus::Untranslated: return i18nc("@info:status", "Untranslated"); } } else if (role == Qt::DecorationRole) { if (column != CatalogModelColumns::TranslationStatus) { return {}; } switch (getTranslationStatus(index.row())) { case TranslationStatus::Ready: return QIcon::fromTheme("emblem-checked"); case TranslationStatus::NeedsReview: return QIcon::fromTheme("emblem-question"); case TranslationStatus::Untranslated: return QIcon::fromTheme("emblem-unavailable"); } } else if (role == Qt::UserRole) { switch (column) { case CatalogModelColumns::TranslationStatus: return m_catalog->isApproved(index.row()); case CatalogModelColumns::IsEmpty: return m_catalog->isEmpty(index.row()); case CatalogModelColumns::State: return int(m_catalog->state(index.row())); case CatalogModelColumns::IsModified: return m_catalog->isModified(index.row()); case CatalogModelColumns::IsPlural: return m_catalog->isPlural(index.row()); default: role = Qt::DisplayRole; } } else if (role == StringFilterRole) { //exclude UI strings if (column >= CatalogModelColumns::TranslationStatus) return QVariant(); else if (column == CatalogModelColumns::Source || column == CatalogModelColumns::Target) { QString str = column == CatalogModelColumns::Source ? m_catalog->msgidWithPlurals(index.row(), false) : m_catalog->msgstrWithPlurals(index.row(), false); return m_ignoreAccel ? str.remove(Project::instance()->accel()) : str; } role = Qt::DisplayRole; } else if (role == SortRole) { //exclude UI strings if (column == CatalogModelColumns::TranslationStatus) { return static_cast(getTranslationStatus(index.row())); } role = Qt::DisplayRole; } if (role != Qt::DisplayRole) return QVariant(); switch (column) { case CatalogModelColumns::Key: return index.row() + 1; case CatalogModelColumns::Source: return m_catalog->msgidWithPlurals(index.row(), true); case CatalogModelColumns::Target: return m_catalog->msgstrWithPlurals(index.row(), true); case CatalogModelColumns::Notes: { QString result; foreach (const Note ¬e, m_catalog->notes(index.row())) result += note.content; return result; } case CatalogModelColumns::Context: return m_catalog->context(index.row()); case CatalogModelColumns::Files: return m_catalog->sourceFiles(index.row()).join('|'); + case CatalogModelColumns::SourceLength: + return m_catalog->msgidWithPlurals(index.row(), true).length(); + case CatalogModelColumns::TargetLength: + return m_catalog->msgstrWithPlurals(index.row(), true).length(); default: return {}; } } CatalogTreeModel::TranslationStatus CatalogTreeModel::getTranslationStatus(int row) const { if (m_catalog->isEmpty(row)) { return CatalogTreeModel::TranslationStatus::Untranslated; } if (m_catalog->isApproved(row)) { return CatalogTreeModel::TranslationStatus::Ready; } else { return CatalogTreeModel::TranslationStatus::NeedsReview; } } CatalogTreeFilterModel::CatalogTreeFilterModel(QObject* parent) : QSortFilterProxyModel(parent) , m_filterOptions(AllStates) , m_individualRejectFilterEnable(false) , m_mergeCatalog(NULL) { setFilterKeyColumn(-1); setFilterCaseSensitivity(Qt::CaseInsensitive); setFilterRole(CatalogTreeModel::StringFilterRole); setSortRole(CatalogTreeModel::SortRole); setDynamicSortFilter(false); } void CatalogTreeFilterModel::setSourceModel(QAbstractItemModel* sourceModel) { QSortFilterProxyModel::setSourceModel(sourceModel); connect(sourceModel, &QAbstractItemModel::modelReset, this, QOverload<>::of(&CatalogTreeFilterModel::setEntriesFilteredOut)); setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut() { return setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut(bool filteredOut) { m_individualRejectFilter.fill(filteredOut, sourceModel()->rowCount()); m_individualRejectFilterEnable = filteredOut; invalidateFilter(); } void CatalogTreeFilterModel::setEntryFilteredOut(int entry, bool filteredOut) { // if (entry>=m_individualRejectFilter.size()) // sourceModelReset(); m_individualRejectFilter[entry] = filteredOut; m_individualRejectFilterEnable = true; invalidateFilter(); } void CatalogTreeFilterModel::setFilterOptions(int o) { m_filterOptions = o; setFilterCaseSensitivity(o & CaseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive); static_cast(sourceModel())->setIgnoreAccel(o & IgnoreAccel); invalidateFilter(); } bool CatalogTreeFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { int filerOptions = m_filterOptions; bool accepts = true; if (bool(filerOptions & Ready) != bool(filerOptions & NotReady)) { bool ready = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::TranslationStatus), source_parent).data(Qt::UserRole).toBool(); accepts = (ready == bool(filerOptions & Ready) || ready != bool(filerOptions & NotReady)); } if (accepts && bool(filerOptions & NonEmpty) != bool(filerOptions & Empty)) { bool untr = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsEmpty), source_parent).data(Qt::UserRole).toBool(); accepts = (untr == bool(filerOptions & Empty) || untr != bool(filerOptions & NonEmpty)); } if (accepts && bool(filerOptions & Modified) != bool(filerOptions & NonModified)) { bool modified = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsModified), source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Modified) || modified != bool(filerOptions & NonModified)); } if (accepts && bool(filerOptions & Plural) != bool(filerOptions & NonPlural)) { bool modified = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsPlural), source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Plural) || modified != bool(filerOptions & NonPlural)); } // These are the possible sync options of a row: // * SameInSync: The sync file contains a row with the same msgid and the same msgstr. // * DifferentInSync: The sync file contains a row with the same msgid and different msgstr. // * NotInSync: The sync file does not contain any row with the same msgid. // // The code below takes care of filtering rows when any of those options is not checked. // const int mask = (SameInSync | DifferentInSync | NotInSync); if (accepts && m_mergeCatalog && (filerOptions & mask) && (filerOptions & mask) != mask) { bool isPresent = m_mergeCatalog->isPresent(source_row); bool isDifferent = m_mergeCatalog->isDifferent(source_row); accepts = ! ((isPresent && !isDifferent && !bool(filerOptions & SameInSync)) || (isPresent && isDifferent && !bool(filerOptions & DifferentInSync)) || (!isPresent && !bool(filerOptions & NotInSync)) ); } if (accepts && (filerOptions & STATES) != STATES) { int state = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::State), source_parent).data(Qt::UserRole).toInt(); accepts = (filerOptions & (1 << (state + FIRSTSTATEPOSITION))); } accepts = accepts && !(m_individualRejectFilterEnable && source_row < m_individualRejectFilter.size() && m_individualRejectFilter.at(source_row)); return accepts && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } void CatalogTreeFilterModel::setMergeCatalogPointer(MergeCatalog* pointer) { m_mergeCatalog = pointer; } diff --git a/src/cataloglistview/catalogmodel.h b/src/cataloglistview/catalogmodel.h index 2b75f97..bc112c9 100644 --- a/src/cataloglistview/catalogmodel.h +++ b/src/cataloglistview/catalogmodel.h @@ -1,198 +1,200 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2013 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef CATALOGMODEL_H #define CATALOGMODEL_H #include "mergecatalog.h" #include "pos.h" #include #include #include class Catalog; /** * MVC wrapper for Catalog */ class CatalogTreeModel: public QAbstractItemModel { Q_OBJECT public: enum class CatalogModelColumns { Key = 0, Source, Target, Notes, Context, Files, TranslationStatus, + SourceLength, + TargetLength, IsEmpty, State, IsModified, IsPlural, ColumnCount, }; - static const int DisplayedColumnCount = static_cast(CatalogModelColumns::TranslationStatus) + 1; + static const int DisplayedColumnCount = static_cast(CatalogModelColumns::TargetLength) + 1; // Possible values in column "Translation Status". enum class TranslationStatus { // translated Ready, // fuzzy NeedsReview, // empty Untranslated, }; enum Roles { StringFilterRole = Qt::UserRole + 1, SortRole = Qt::UserRole + 2, }; explicit CatalogTreeModel(QObject* parent, Catalog* catalog); ~CatalogTreeModel() override = default; QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex())const override; QModelIndex parent(const QModelIndex&) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; Catalog* catalog()const { return m_catalog; } void setIgnoreAccel(bool n) { m_ignoreAccel = n; } public slots: void reflectChanges(DocPosition); void fileLoaded(); private: TranslationStatus getTranslationStatus(int row) const; Catalog* m_catalog; bool m_ignoreAccel; static QVector m_fonts; //DocPos m_prevChanged; }; class CatalogTreeFilterModel: public QSortFilterProxyModel { Q_OBJECT public: enum FilterOptions { CaseInsensitive = 1 << 0, IgnoreAccel = 1 << 1, Ready = 1 << 2, NotReady = 1 << 3, NonEmpty = 1 << 4, Empty = 1 << 5, Modified = 1 << 6, NonModified = 1 << 7, SameInSync = 1 << 8, DifferentInSync = 1 << 9, NotInSync = 1 << 10, Plural = 1 << 11, NonPlural = 1 << 12, //states (see defines below) New = 1 << 13, NeedsTranslation = 1 << 14, NeedsL10n = 1 << 15, NeedsAdaptation = 1 << 16, Translated = 1 << 17, NeedsReviewTranslation = 1 << 18, NeedsReviewL10n = 1 << 19, NeedsReviewAdaptation = 1 << 20, Final = 1 << 21, SignedOff = 1 << 22, MaxOption = 1 << 23, AllStates = MaxOption - 1 }; #define STATES ((0xffff<<13)&(AllStates)) #define FIRSTSTATEPOSITION 13 explicit CatalogTreeFilterModel(QObject* parent); ~CatalogTreeFilterModel() {} bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; void setFilterOptions(int o); int filterOptions()const { return m_filterOptions; } void setSourceModel(QAbstractItemModel* sourceModel) override; bool individualRejectFilterEnabled() { return m_individualRejectFilterEnable; } void setEntryFilteredOut(int entry, bool filteredOut); void setMergeCatalogPointer(MergeCatalog* pointer); public slots: void setEntriesFilteredOut(); void setEntriesFilteredOut(bool filteredOut); void setDynamicSortFilter(bool enabled) { QSortFilterProxyModel::setDynamicSortFilter(enabled); } private: int m_filterOptions; bool m_individualRejectFilterEnable; QVector m_individualRejectFilter; //used from kross scripts MergeCatalog* m_mergeCatalog; }; #endif diff --git a/src/editortab_findreplace.cpp b/src/editortab_findreplace.cpp index 7aced70..cf5d1a2 100644 --- a/src/editortab_findreplace.cpp +++ b/src/editortab_findreplace.cpp @@ -1,620 +1,619 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalize_debug.h" #include "editortab.h" #include "editorview.h" #include "catalog.h" #include "pos.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "ui_kaider_findextension.h" #include "stemming.h" #include #include #include #include #include #include #include #include #include #define IGNOREACCELS KFind::MinimumUserOption #define INCLUDENOTES KFind::MinimumUserOption*2 static long makeOptions(long options, const Ui_findExtension* ui_findExtension) { return options + IGNOREACCELS * ui_findExtension->m_ignoreAccelMarks->isChecked() + INCLUDENOTES * ui_findExtension->m_notes->isChecked(); //bool skipMarkup(){return ui_findExtension->m_skipTags->isChecked();} } class EntryFindDialog: public KFindDialog { public: EntryFindDialog(QWidget* parent); ~EntryFindDialog(); long options() const { return makeOptions(KFindDialog::options(), ui_findExtension); } static EntryFindDialog* instance(QWidget* parent = nullptr); private: static QPointer _instance; static void cleanup() { delete EntryFindDialog::_instance; } private: Ui_findExtension* ui_findExtension; }; QPointer EntryFindDialog::_instance = 0; EntryFindDialog* EntryFindDialog::instance(QWidget* parent) { if (_instance == 0) { _instance = new EntryFindDialog(parent); qAddPostRoutine(EntryFindDialog::cleanup); } return _instance; } EntryFindDialog::EntryFindDialog(QWidget* parent) : KFindDialog(parent) , ui_findExtension(new Ui_findExtension) { ui_findExtension->setupUi(findExtension()); setHasSelection(false); KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); setOptions(stateGroup.readEntry("FindOptions", (qlonglong)0)); setFindHistory(stateGroup.readEntry("FindHistory", QStringList())); } EntryFindDialog::~EntryFindDialog() { KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); stateGroup.writeEntry("FindOptions", (qlonglong)options()); stateGroup.writeEntry("FindHistory", findHistory()); delete ui_findExtension; } //BEGIN EntryReplaceDialog class EntryReplaceDialog: public KReplaceDialog { public: EntryReplaceDialog(QWidget* parent); ~EntryReplaceDialog(); long options() const { return makeOptions(KReplaceDialog::options(), ui_findExtension); } static EntryReplaceDialog* instance(QWidget* parent = nullptr); private: static QPointer _instance; static void cleanup() { delete EntryReplaceDialog::_instance; } private: Ui_findExtension* ui_findExtension; }; QPointer EntryReplaceDialog::_instance = 0; EntryReplaceDialog* EntryReplaceDialog::instance(QWidget* parent) { if (_instance == 0) { _instance = new EntryReplaceDialog(parent); qAddPostRoutine(EntryReplaceDialog::cleanup); } return _instance; } EntryReplaceDialog::EntryReplaceDialog(QWidget* parent) : KReplaceDialog(parent) , ui_findExtension(new Ui_findExtension) { ui_findExtension->setupUi(findExtension()); //ui_findExtension->m_notes->hide(); setHasSelection(false); KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); setOptions(stateGroup.readEntry("ReplaceOptions", (qlonglong)0)); setFindHistory(stateGroup.readEntry("ReplacePatternHistory", QStringList())); setReplacementHistory(stateGroup.readEntry("ReplacementHistory", QStringList())); } EntryReplaceDialog::~EntryReplaceDialog() { KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); stateGroup.writeEntry("ReplaceOptions", (qlonglong)options()); stateGroup.writeEntry("ReplacePatternHistory", findHistory()); stateGroup.writeEntry("ReplacementHistory", replacementHistory()); delete ui_findExtension; } //END EntryReplaceDialog //TODO &,   static void calcOffsetWithAccels(const QString& data, int& offset, int& length) { int i = 0; for (; i < offset; ++i) if (Q_UNLIKELY(data.at(i) == '&')) ++offset; //if & is inside highlighted word int limit = offset + length; for (i = offset; i < limit; ++i) if (Q_UNLIKELY(data.at(i) == '&')) { ++length; limit = qMin(data.size(), offset + length); //just safety } } static bool determineStartingPos(Catalog* m_catalog, KFind* find, DocPosition& pos) //search or replace //called from find() and findNext() { if (find->options() & KFind::FindBackwards) { pos.entry = m_catalog->numberOfEntries() - 1; pos.form = (m_catalog->isPlural(pos.entry)) ? m_catalog->numberOfPluralForms() - 1 : 0; } else { pos.entry = 0; pos.form = 0; } return true; } void EditorTab::find() { //QWidget* p=0; QWidget* next=qobject_cast(parent()); while(next) { p=next; next=qobject_cast(next->parent()); } EntryFindDialog::instance(nativeParentWidget()); QString sel = selectionInTarget(); if (!(sel.isEmpty() && selectionInSource().isEmpty())) { if (sel.isEmpty()) sel = selectionInSource(); if (m_find && m_find->options()&IGNOREACCELS) sel.remove('&'); EntryFindDialog::instance()->setPattern(sel); } if (EntryFindDialog::instance()->exec() != QDialog::Accepted) return; if (m_find) { m_find->resetCounts(); m_find->setPattern(EntryFindDialog::instance()->pattern()); m_find->setOptions(EntryFindDialog::instance()->options()); } else { // This creates a find-next-prompt dialog if needed. m_find = new KFind(EntryFindDialog::instance()->pattern(), EntryFindDialog::instance()->options(), this, EntryFindDialog::instance()); connect(m_find, QOverload::of(&KFind::highlight), this, &EditorTab::highlightFound); connect(m_find, &KFind::findNext, this, QOverload<>::of(&EditorTab::findNext)); m_find->closeFindNextDialog(); } DocPosition pos; if (m_find->options() & KFind::FromCursor) pos = m_currentPos; else if (!determineStartingPos(m_catalog, m_find, pos)) return; findNext(pos); } void EditorTab::findNext(const DocPosition& startingPos) { Catalog& catalog = *m_catalog; KFind& find = *m_find; if (Q_UNLIKELY(catalog.numberOfEntries() <= startingPos.entry)) return;//for the case when app wasn't able to process event before file close bool anotherEntry = _searchingPos.entry != m_currentPos.entry; _searchingPos = startingPos; if (anotherEntry) _searchingPos.offset = 0; QRegExp rx("[^(\\\\n)>]\n"); - QElapsedTimer a; a.start(); //_searchingPos.part=DocPosition::Source; bool ignoreaccels = m_find->options()&IGNOREACCELS; bool includenotes = m_find->options()&INCLUDENOTES; int switchOptions = DocPosition::Source | DocPosition::Target | (includenotes * DocPosition::Comment); int flag = 1; while (flag) { flag = 0; KFind::Result res = KFind::NoMatch; while (true) { if (find.needData() || anotherEntry || m_view->m_modifiedAfterFind) { anotherEntry = false; m_view->m_modifiedAfterFind = false; QString data; if (_searchingPos.part == DocPosition::Comment) data = catalog.notes(_searchingPos).at(_searchingPos.form).content; else data = catalog.catalogString(_searchingPos).string; if (ignoreaccels) data.remove('&'); find.setData(data); } res = find.find(); //offset=-1; if (res != KFind::NoMatch) break; if (!( (find.options()&KFind::FindBackwards) ? switchPrev(m_catalog, _searchingPos, switchOptions) : switchNext(m_catalog, _searchingPos, switchOptions) )) break; } if (res == KFind::NoMatch) { //file-wide search if (find.shouldRestart(true, true)) { flag = 1; determineStartingPos(m_catalog, m_find, _searchingPos); } find.resetCounts(); } } } void EditorTab::findNext() { if (m_find) { findNext((m_currentPos.entry == _searchingPos.entry && _searchingPos.part == DocPosition::Comment) ? _searchingPos : m_currentPos); } else find(); } void EditorTab::findPrev() { if (m_find) { m_find->setOptions(m_find->options() ^ KFind::FindBackwards); findNext(m_currentPos); } else { find(); } } void EditorTab::highlightFound(const QString &, int matchingIndex, int matchedLength) { if (m_find->options()&IGNOREACCELS && _searchingPos.part != DocPosition::Comment) { QString data = m_catalog->catalogString(_searchingPos).string; calcOffsetWithAccels(data, matchingIndex, matchedLength); } _searchingPos.offset = matchingIndex; gotoEntry(_searchingPos, matchedLength); } void EditorTab::replace() { EntryReplaceDialog::instance(nativeParentWidget()); if (!m_view->selectionInTarget().isEmpty()) { if (m_replace && m_replace->options()&IGNOREACCELS) { QString tmp(m_view->selectionInTarget()); tmp.remove('&'); EntryReplaceDialog::instance()->setPattern(tmp); } else EntryReplaceDialog::instance()->setPattern(m_view->selectionInTarget()); } if (EntryReplaceDialog::instance()->exec() != QDialog::Accepted) return; if (m_replace) m_replace->deleteLater();// _replace=0; // This creates a find-next-prompt dialog if needed. { m_replace = new KReplace(EntryReplaceDialog::instance()->pattern(), EntryReplaceDialog::instance()->replacement(), EntryReplaceDialog::instance()->options(), this, EntryReplaceDialog::instance()); connect(m_replace, QOverload::of(&KReplace::highlight), this, &EditorTab::highlightFound_); connect(m_replace, &KReplace::findNext, this, QOverload<>::of(&EditorTab::replaceNext)); connect(m_replace, QOverload::of(&KReplace::replace), this, &EditorTab::doReplace); connect(m_replace, &KReplace::dialogClosed, this, &EditorTab::cleanupReplace); // _replace->closeReplaceNextDialog(); } // else // { // _replace->resetCounts(); // _replace->setPattern(EntryReplaceDialog::instance()->pattern()); // _replace->setOptions(EntryReplaceDialog::instance()->options()); // } //m_catalog->beginMacro(i18nc("@item Undo action item","Replace")); m_doReplaceCalled = false; if (m_replace->options() & KFind::FromCursor) replaceNext(m_currentPos); else { DocPosition pos; if (!determineStartingPos(m_catalog, m_replace, pos)) return; replaceNext(pos); } } void EditorTab::replaceNext(const DocPosition& startingPos) { bool anotherEntry = m_currentPos.entry != _replacingPos.entry; _replacingPos = startingPos; if (anotherEntry) _replacingPos.offset = 0; int flag = 1; bool ignoreaccels = m_replace->options()&IGNOREACCELS; bool includenotes = m_replace->options()&INCLUDENOTES; qCWarning(LOKALIZE_LOG) << "includenotes" << includenotes; int switchOptions = DocPosition::Target | (includenotes * DocPosition::Comment); while (flag) { flag = 0; KFind::Result res = KFind::NoMatch; while (1) { if (m_replace->needData() || anotherEntry/*||m_view->m_modifiedAfterFind*/) { anotherEntry = false; //m_view->m_modifiedAfterFind=false;//NOTE TEST THIS QString data; if (_replacingPos.part == DocPosition::Comment) data = m_catalog->notes(_replacingPos).at(_replacingPos.form).content; else { data = m_catalog->targetWithTags(_replacingPos).string; if (ignoreaccels) data.remove('&'); } m_replace->setData(data); } res = m_replace->replace(); if (res != KFind::NoMatch) break; if (!( (m_replace->options()&KFind::FindBackwards) ? switchPrev(m_catalog, _replacingPos, switchOptions) : switchNext(m_catalog, _replacingPos, switchOptions) )) break; } if (res == KFind::NoMatch) { if ((m_replace->options()&KFind::FromCursor) && m_replace->shouldRestart(true)) { flag = 1; determineStartingPos(m_catalog, m_replace, _replacingPos); } else { if (!(m_replace->options() & KFind::FromCursor)) m_replace->displayFinalDialog(); m_replace->closeReplaceNextDialog(); cleanupReplace(); } m_replace->resetCounts(); } } } void EditorTab::cleanupReplace() { if (m_doReplaceCalled) { m_doReplaceCalled = false; m_catalog->endMacro(); } } void EditorTab::replaceNext() { replaceNext(m_currentPos); } void EditorTab::highlightFound_(const QString &, int matchingIndex, int matchedLength) { if (m_replace->options()&IGNOREACCELS) { QString data = m_catalog->targetWithTags(_replacingPos).string; calcOffsetWithAccels(data, matchingIndex, matchedLength); } _replacingPos.offset = matchingIndex; gotoEntry(_replacingPos, matchedLength); } void EditorTab::doReplace(const QString &newStr, int offset, int newLen, int remLen) { if (!m_doReplaceCalled) { m_doReplaceCalled = true; m_catalog->beginMacro(i18nc("@item Undo action item", "Replace")); } DocPosition pos = _replacingPos; if (_replacingPos.part == DocPosition::Comment) m_catalog->push(new SetNoteCmd(m_catalog, pos, newStr)); else { QString oldStr = m_catalog->target(_replacingPos); if (m_replace->options()&IGNOREACCELS) calcOffsetWithAccels(oldStr, offset, remLen); pos.offset = offset; m_catalog->push(new DelTextCmd(m_catalog, pos, oldStr.mid(offset, remLen))); if (newLen) m_catalog->push(new InsTextCmd(m_catalog, pos, newStr.mid(offset, newLen))); } if (pos.entry == m_currentPos.entry) { pos.offset += newLen; m_view->gotoEntry(pos, 0); } } void EditorTab::spellcheck() { if (!m_sonnetDialog) { m_sonnetChecker = new Sonnet::BackgroundChecker(this); m_sonnetChecker->changeLanguage(enhanceLangCode(Project::instance()->langCode())); m_sonnetDialog = new Sonnet::Dialog(m_sonnetChecker, this); - connect(m_sonnetDialog, QOverload::of(&Sonnet::Dialog::spellCheckDone), this, &EditorTab::spellcheckNext); + connect(m_sonnetDialog, &Sonnet::Dialog::spellCheckDone, this, &EditorTab::spellcheckNext); connect(m_sonnetDialog, &Sonnet::Dialog::replace, this, &EditorTab::spellcheckReplace); connect(m_sonnetDialog, &Sonnet::Dialog::stop, this, &EditorTab::spellcheckStop); connect(m_sonnetDialog, &Sonnet::Dialog::cancel, this, &EditorTab::spellcheckCancel); connect(m_sonnetDialog/*m_sonnetChecker*/, &Sonnet::Dialog::misspelling, this, &EditorTab::spellcheckShow); // disconnect(/*m_sonnetDialog*/m_sonnetChecker,SIGNAL(misspelling(QString,int)), // m_sonnetDialog,SLOT(slotMisspelling(QString,int))); // // connect( d->checker, SIGNAL(misspelling(const QString&, int)), // SLOT(slotMisspelling(const QString&, int)) ); } QString text = m_catalog->msgstr(m_currentPos); if (!m_view->selectionInTarget().isEmpty()) text = m_view->selectionInTarget(); text.remove('&'); m_sonnetDialog->setBuffer(text); _spellcheckPos = m_currentPos; _spellcheckStartPos = m_currentPos; m_spellcheckStop = false; //m_catalog->beginMacro(i18n("Spellcheck")); m_spellcheckStartUndoIndex = m_catalog->index(); m_sonnetDialog->show(); } void EditorTab::spellcheckNext() { if (m_spellcheckStop) return; do { if (!switchNext(m_catalog, _spellcheckPos)) { qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.entry; qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.form; bool continueFromStart = !(_spellcheckStartPos.entry == 0 && _spellcheckStartPos.form == 0) && KMessageBox::questionYesNo(this, i18n("Lokalize has reached end of document. Do you want to continue from start?"), i18nc("@title", "Spellcheck")) == KMessageBox::Yes; if (continueFromStart) { _spellcheckStartPos.entry = 0; _spellcheckStartPos.form = 0; _spellcheckPos = _spellcheckStartPos; } else { KMessageBox::information(this, i18n("Lokalize has finished spellchecking"), i18nc("@title", "Spellcheck")); return; } } } while (m_catalog->msgstr(_spellcheckPos).isEmpty() || !m_catalog->isApproved(_spellcheckPos.entry)); m_sonnetDialog->setBuffer(m_catalog->msgstr(_spellcheckPos).remove(Project::instance()->accel())); } void EditorTab::spellcheckStop() { m_spellcheckStop = true; } void EditorTab::spellcheckCancel() { m_catalog->setIndex(m_spellcheckStartUndoIndex); gotoEntry(_spellcheckPos); } void EditorTab::spellcheckShow(const QString &word, int offset) { const Project& project = *Project::instance(); const QString accel = project.accel(); QString source = m_catalog->source(_spellcheckPos); source.remove(accel); if (source.contains(word) && project.targetLangCode().leftRef(2) != project.sourceLangCode().leftRef(2)) { m_sonnetDialog->setUpdatesEnabled(false); m_sonnetChecker->continueChecking(); return; } m_sonnetDialog->setUpdatesEnabled(true); show(); DocPosition pos = _spellcheckPos; int length = word.length(); calcOffsetWithAccels(m_catalog->target(pos), offset, length); pos.offset = offset; gotoEntry(pos, length); } void EditorTab::spellcheckReplace(QString oldWord, int offset, const QString &newWord) { DocPosition pos = _spellcheckPos; int length = oldWord.length(); calcOffsetWithAccels(m_catalog->target(pos), offset, length); pos.offset = offset; if (length > oldWord.length()) //replaced word contains accel mark oldWord = m_catalog->target(pos).mid(offset, length); m_catalog->push(new DelTextCmd(m_catalog, pos, oldWord)); m_catalog->push(new InsTextCmd(m_catalog, pos, newWord)); gotoEntry(pos, newWord.length()); } diff --git a/src/filesearch/filesearchtab.cpp b/src/filesearch/filesearchtab.cpp index 11058a6..380a8f2 100644 --- a/src/filesearch/filesearchtab.cpp +++ b/src/filesearch/filesearchtab.cpp @@ -1,922 +1,923 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "filesearchtab.h" #include "lokalize_debug.h" #include "ui_filesearchoptions.h" #include "ui_massreplaceoptions.h" #include "project.h" #include "prefs.h" #include "tmscanapi.h" //TODO separate some decls into new header #include "state.h" #include "qaview.h" #include "catalog.h" #include "fastsizehintitemdelegate.h" #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QStringList doScanRecursive(const QDir& dir); class FileListModel: public QStringListModel { public: FileListModel(QObject* parent): QStringListModel(parent) {} QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex&) const override { return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } }; QVariant FileListModel::data(const QModelIndex& item, int role) const { if (role == Qt::DisplayRole) return shorterFilePath(stringList().at(item.row())); if (role == Qt::UserRole) return stringList().at(item.row()); return QVariant(); } SearchFileListView::SearchFileListView(QWidget* parent) : QDockWidget(i18nc("@title:window", "File List"), parent) , m_browser(new QTreeView(this)) , m_background(new QLabel(i18n("Drop translation files here..."), this)) , m_model(new FileListModel(this)) { setWidget(m_background); m_background->setMinimumWidth(QFontMetrics(font()).averageCharWidth() * 30); m_background->setAlignment(Qt::AlignCenter); m_browser->hide(); m_browser->setModel(m_model); m_browser->setRootIsDecorated(false); m_browser->setHeaderHidden(true); m_browser->setUniformRowHeights(true); m_browser->setAlternatingRowColors(true); m_browser->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* action = new QAction(i18nc("@action:inmenu", "Clear"), m_browser); connect(action, &QAction::triggered, this, &SearchFileListView::clear); m_browser->addAction(action); connect(m_browser, &QTreeView::activated, this, &SearchFileListView::requestFileOpen); } void SearchFileListView::requestFileOpen(const QModelIndex& item) { emit fileOpenRequested(item.data(Qt::UserRole).toString(), true); } void SearchFileListView::addFiles(const QStringList& files) { if (files.isEmpty()) return; m_background->hide(); setWidget(m_browser); m_browser->show(); //ensure unquiness, sorting the list along the way QMap map; foreach (const QString& filepath, m_model->stringList()) map[filepath] = true; foreach (const QString& filepath, files) map[filepath] = true; m_model->setStringList(map.keys()); } void SearchFileListView::addFilesFast(const QStringList& files) { if (files.size()) m_model->setStringList(m_model->stringList() + files); } void SearchFileListView::clear() { m_model->setStringList(QStringList()); } QStringList SearchFileListView::files() const { return m_model->stringList(); } void SearchFileListView::scrollTo(const QString& file) { if (file.isEmpty()) { m_browser->scrollToTop(); return; } int idx = m_model->stringList().indexOf(file); if (idx != -1) m_browser->scrollTo(m_model->index(idx, 0), QAbstractItemView::PositionAtCenter); } bool SearchParams::isEmpty() const { return sourcePattern.pattern().isEmpty() && targetPattern.pattern().isEmpty(); } SearchJob::SearchJob(const QStringList& f, const SearchParams& sp, const QVector& r, int sn, QObject*) : QRunnable() , files(f) , searchParams(sp) , rules(r) , searchNumber(sn) , m_size(0) { setAutoDelete(false); } void SearchJob::run() { - QTime a; a.start(); + QElapsedTimer a; a.start(); bool removeAmpFromSource = searchParams.sourcePattern.patternSyntax() == QRegExp::FixedString && !searchParams.sourcePattern.pattern().contains(QLatin1Char('&')); bool removeAmpFromTarget = searchParams.targetPattern.patternSyntax() == QRegExp::FixedString && !searchParams.targetPattern.pattern().contains(QLatin1Char('&')); foreach (const QString& filePath, files) { Catalog catalog(0); if (Q_UNLIKELY(catalog.loadFromUrl(filePath, QString(), &m_size, true) != 0)) continue; //QVector catalogResults; int numberOfEntries = catalog.numberOfEntries(); DocPosition pos(0); for (; pos.entry < numberOfEntries; pos.entry++) { //if (!searchParams.states[catalog.state(pos)]) // return false; int lim = catalog.isPlural(pos.entry) ? catalog.numberOfPluralForms() : 1; for (pos.form = 0; pos.form < lim; pos.form++) { int sp = 0; int tp = 0; if (!searchParams.sourcePattern.isEmpty()) sp = searchParams.sourcePattern.indexIn(removeAmpFromSource ? catalog.source(pos).remove(QLatin1Char('&')) : catalog.source(pos)); if (!searchParams.targetPattern.isEmpty()) tp = searchParams.targetPattern.indexIn(removeAmpFromTarget ? catalog.target(pos).remove(QLatin1Char('&')) : catalog.target(pos)); //int np=searchParams.notesPattern.indexIn(catalog.notes(pos)); if ((sp != -1) != searchParams.invertSource && (tp != -1) != searchParams.invertTarget) { //TODO handle multiple results in same column //FileSearchResult r; SearchResult r; r.filepath = filePath; r.docPos = pos; if (!searchParams.sourcePattern.isEmpty() && !searchParams.invertSource) r.sourcePositions << StartLen(searchParams.sourcePattern.pos(), searchParams.sourcePattern.matchedLength()); if (!searchParams.targetPattern.isEmpty() && !searchParams.invertTarget) r.targetPositions << StartLen(searchParams.targetPattern.pos(), searchParams.targetPattern.matchedLength()); r.source = catalog.source(pos); r.target = catalog.target(pos); r.state = catalog.state(pos); r.isApproved = catalog.isApproved(pos); //r.activePhase=catalog.activePhase(); if (rules.size()) { QVector positions(2); int matchedQaRule = findMatchingRule(rules, r.source, r.target, positions); if (matchedQaRule == -1) continue; if (positions.at(0).len) r.sourcePositions << positions.at(0); if (positions.at(1).len) r.targetPositions << positions.at(1); } r.sourcePositions.squeeze(); r.targetPositions.squeeze(); //catalogResults< map; for (int i = 0; i < searchResults.count(); ++i) map.insertMulti(searchResults.at(i).filepath, i); foreach (const QString& filepath, map.keys()) { Catalog catalog(QThread::currentThread()); if (catalog.loadFromUrl(filepath, QString()) != 0) continue; foreach (int index, map.values(filepath)) { SearchResult& sr = searchResults[index]; DocPosition docPos = sr.docPos.toDocPosition(); if (catalog.target(docPos) != sr.target) { qCWarning(LOKALIZE_LOG) << "skipping replace because" << catalog.target(docPos) << "!=" << sr.target; continue; } CatalogString s = catalog.targetWithTags(docPos); int pos = replaceWhat.indexIn(s.string); while (pos != -1) { if (!s.string.midRef(pos, replaceWhat.matchedLength()).contains(TAGRANGE_IMAGE_SYMBOL)) { docPos.offset = pos; catalog.targetDelete(docPos, replaceWhat.matchedLength()); catalog.targetInsert(docPos, replaceWith); s.string.replace(pos, replaceWhat.matchedLength(), replaceWith); pos += replaceWith.length(); } else { pos += replaceWhat.matchedLength(); qCWarning(LOKALIZE_LOG) << "skipping replace because matched text contains markup" << s.string; } if (pos > s.string.length() || replaceWhat.pattern().startsWith('^')) break; pos = replaceWhat.indexIn(s.string, pos); } } catalog.save(); } } //BEGIN FileSearchModel FileSearchModel::FileSearchModel(QObject* parent) : QAbstractListModel(parent) { } QVariant FileSearchModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case FileSearchModel::Source: return i18nc("@title:column Original text", "Source"); case FileSearchModel::Target: return i18nc("@title:column Text in target language", "Target"); //case FileSearchModel::Context: return i18nc("@title:column","Context"); case FileSearchModel::Filepath: return i18nc("@title:column", "File"); case FileSearchModel::TranslationStatus: return i18nc("@title:column", "Translation Status"); } return QVariant(); } void FileSearchModel::appendSearchResults(const SearchResults& results) { beginInsertRows(QModelIndex(), m_searchResults.size(), m_searchResults.size() + results.size() - 1); m_searchResults += results; endInsertRows(); } void FileSearchModel::clear() { beginResetModel(); m_searchResults.clear();; endResetModel(); } QVariant FileSearchModel::data(const QModelIndex& item, int role) const { bool doHtml = (role == FastSizeHintItemDelegate::HtmlDisplayRole); if (doHtml) role = Qt::DisplayRole; if (role == Qt::DisplayRole) { QString result; const SearchResult& sr = m_searchResults.at(item.row()); if (item.column() == Source) result = sr.source; if (item.column() == Target) result = sr.target; if (item.column() == Filepath) result = shorterFilePath(sr.filepath); if (doHtml && item.column() <= FileSearchModel::Target) { if (result.isEmpty()) return result; const QString startBld = QStringLiteral("_ST_"); const QString endBld = QStringLiteral("_END_"); const QString startBldTag = QStringLiteral(""); const QString endBldTag = QStringLiteral(""); if (item.column() == FileSearchModel::Target && !m_replaceWhat.isEmpty()) { result.replace(m_replaceWhat, m_replaceWith); QString escaped = convertToHtml(result, !sr.isApproved); escaped.replace(startBld, startBldTag); escaped.replace(endBld, endBldTag); return escaped; } const QVector& occurrences = item.column() == FileSearchModel::Source ? sr.sourcePositions : sr.targetPositions; int occ = occurrences.count(); while (--occ >= 0) { const StartLen& sl = occurrences.at(occ); result.insert(sl.start + sl.len, endBld); result.insert(sl.start, startBld); } /* !isApproved(sr.state, Project::instance()->local()->role())*/ QString escaped = convertToHtml(result, item.column() == FileSearchModel::Target && !sr.isApproved); escaped.replace(startBld, startBldTag); escaped.replace(endBld, endBldTag); return escaped; } return result; } if (role == Qt::UserRole) { const SearchResult& sr = m_searchResults.at(item.row()); if (item.column() == Filepath) return sr.filepath; } return QVariant(); } void FileSearchModel::setReplacePreview(const QRegExp& s, const QString& r) { m_replaceWhat = s; m_replaceWith = QLatin1String("_ST_") + r + QLatin1String("_END_"); emit dataChanged(index(0, Target), index(rowCount() - 1, Target)); } //END FileSearchModel //BEGIN FileSearchTab FileSearchTab::FileSearchTab(QWidget *parent) : LokalizeSubwindowBase2(parent) // , m_proxyModel(new TMResultsSortFilterProxyModel(this)) , m_model(new FileSearchModel(this)) , m_lastSearchNumber(0) , m_dbusId(-1) { setWindowTitle(i18nc("@title:window", "Search and replace in files")); setAcceptDrops(true); QWidget* w = new QWidget(this); ui_fileSearchOptions = new Ui_FileSearchOptions; ui_fileSearchOptions->setupUi(w); setCentralWidget(w); QShortcut* sh = new QShortcut(Qt::CTRL + Qt::Key_L, this); connect(sh, &QShortcut::activated, ui_fileSearchOptions->querySource, QOverload<>::of(&QLineEdit::setFocus)); setFocusProxy(ui_fileSearchOptions->querySource); sh = new QShortcut(Qt::Key_Escape, this, SLOT(stopSearch()), 0, Qt::WidgetWithChildrenShortcut); QTreeView* view = ui_fileSearchOptions->treeView; QVector singleLineColumns(FileSearchModel::ColumnCount, false); singleLineColumns[FileSearchModel::Filepath] = true; singleLineColumns[FileSearchModel::TranslationStatus] = true; //singleLineColumns[TMDBModel::Context]=true; QVector richTextColumns(FileSearchModel::ColumnCount, false); richTextColumns[FileSearchModel::Source] = true; richTextColumns[FileSearchModel::Target] = true; view->setItemDelegate(new FastSizeHintItemDelegate(this, singleLineColumns, richTextColumns)); connect(m_model, &FileSearchModel::modelReset, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_model, &FileSearchModel::dataChanged, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); //connect(m_model,SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),view->itemDelegate(),SLOT(reset())); //connect(m_proxyModel,SIGNAL(layoutChanged()),view->itemDelegate(),SLOT(reset())); //connect(m_proxyModel,SIGNAL(layoutChanged()),this,SLOT(displayTotalResultCount())); view->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* a = new QAction(i18n("Copy source to clipboard"), view); a->setShortcut(Qt::CTRL + Qt::Key_S); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::copySourceToClipboard); view->addAction(a); a = new QAction(i18n("Copy target to clipboard"), view); a->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::copyTargetToClipboard); view->addAction(a); a = new QAction(i18n("Open file"), view); a->setShortcut(QKeySequence(Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::openFile); connect(view, &QTreeView::activated, this, &FileSearchTab::openFile); view->addAction(a); connect(ui_fileSearchOptions->querySource, &QLineEdit::returnPressed, this, &FileSearchTab::performSearch); connect(ui_fileSearchOptions->queryTarget, &QLineEdit::returnPressed, this, &FileSearchTab::performSearch); connect(ui_fileSearchOptions->doFind, &QPushButton::clicked, this, &FileSearchTab::performSearch); // m_proxyModel->setDynamicSortFilter(true); // m_proxyModel->setSourceModel(m_model); view->setModel(m_model); // view->setModel(m_proxyModel); // view->sortByColumn(FileSearchModel::Filepath,Qt::AscendingOrder); // view->setSortingEnabled(true); // view->setItemDelegate(new FastSizeHintItemDelegate(this)); // connect(m_model,SIGNAL(resultsFetched()),view->itemDelegate(),SLOT(reset())); // connect(m_model,SIGNAL(modelReset()),view->itemDelegate(),SLOT(reset())); // connect(m_proxyModel,SIGNAL(layoutChanged()),view->itemDelegate(),SLOT(reset())); // connect(m_proxyModel,SIGNAL(layoutChanged()),this,SLOT(displayTotalResultCount())); //BEGIN resizeColumnToContents static const int maxInitialWidths[] = {QGuiApplication::primaryScreen()->availableGeometry().width() / 3, QGuiApplication::primaryScreen()->availableGeometry().width() / 3}; int column = sizeof(maxInitialWidths) / sizeof(int); while (--column >= 0) view->setColumnWidth(column, maxInitialWidths[column]); //END resizeColumnToContents int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); setXMLFile(QStringLiteral("filesearchtabui.rc"), true); dbusObjectPath(); KActionCollection* ac = actionCollection(); KActionCategory* srf = new KActionCategory(i18nc("@title actions category", "Search and replace in files"), ac); m_searchFileListView = new SearchFileListView(this); //m_searchFileListView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_searchFileListView); srf->addAction(QStringLiteral("showfilelist_action"), m_searchFileListView->toggleViewAction()); connect(m_searchFileListView, &SearchFileListView::fileOpenRequested, this, QOverload::of(&FileSearchTab::fileOpenRequested)); m_massReplaceView = new MassReplaceView(this); addDockWidget(Qt::RightDockWidgetArea, m_massReplaceView); srf->addAction(QStringLiteral("showmassreplace_action"), m_massReplaceView->toggleViewAction()); connect(m_massReplaceView, &MassReplaceView::previewRequested, m_model, &FileSearchModel::setReplacePreview); connect(m_massReplaceView, &MassReplaceView::replaceRequested, this, &FileSearchTab::massReplace); //m_massReplaceView->hide(); m_qaView = new QaView(this); m_qaView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_qaView); srf->addAction(QStringLiteral("showqa_action"), m_qaView->toggleViewAction()); connect(m_qaView, &QaView::rulesChanged, this, &FileSearchTab::performSearch); connect(m_qaView->toggleViewAction(), &QAction::toggled, this, &FileSearchTab::performSearch, Qt::QueuedConnection); view->header()->restoreState(readUiState("FileSearchResultsHeaderState")); } FileSearchTab::~FileSearchTab() { stopSearch(); writeUiState("FileSearchResultsHeaderState", ui_fileSearchOptions->treeView->header()->saveState()); ids.removeAll(m_dbusId); } void FileSearchTab::performSearch() { if (m_searchFileListView->files().isEmpty()) { addFilesToSearch(doScanRecursive(QDir(Project::instance()->poDir()))); if (m_searchFileListView->files().isEmpty()) return; } m_model->clear(); statusBarItems.insert(1, QString()); m_searchFileListView->scrollTo(); m_lastSearchNumber++; SearchParams sp; sp.sourcePattern.setPattern(ui_fileSearchOptions->querySource->text()); sp.targetPattern.setPattern(ui_fileSearchOptions->queryTarget->text()); sp.invertSource = ui_fileSearchOptions->invertSource->isChecked(); sp.invertTarget = ui_fileSearchOptions->invertTarget->isChecked(); QVector rules = m_qaView->isVisible() ? m_qaView->rules() : QVector(); if (sp.isEmpty() && rules.isEmpty()) return; if (!ui_fileSearchOptions->regEx->isChecked()) { sp.sourcePattern.setPatternSyntax(QRegExp::FixedString); sp.targetPattern.setPatternSyntax(QRegExp::FixedString); } /* else { sp.sourcePattern.setMinimal(true); sp.targetPattern.setMinimal(true); } */ stopSearch(); m_massReplaceView->deactivatePreview(); QStringList files = m_searchFileListView->files(); for (int i = 0; i < files.size(); i += 100) { QStringList batch; int lim = qMin(files.size(), i + 100); for (int j = i; j < lim; j++) batch.append(files.at(j)); SearchJob* job = new SearchJob(batch, sp, rules, m_lastSearchNumber); QObject::connect(job, &SearchJob::done, this, &FileSearchTab::searchJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } void FileSearchTab::stopSearch() { #if QT_VERSION >= 0x050500 int i = m_runningJobs.size(); while (--i >= 0) QThreadPool::globalInstance()->tryTake(m_runningJobs.at(i)); #endif m_runningJobs.clear(); } void FileSearchTab::massReplace(const QRegExp &what, const QString& with) { #define BATCH_SIZE 20 SearchResults searchResults = m_model->searchResults(); for (int i = 0; i < searchResults.count(); i += BATCH_SIZE) { int last = qMin(i + BATCH_SIZE, searchResults.count() - 1); QString filepath = searchResults.at(last).filepath; while (last + 1 < searchResults.count() && filepath == searchResults.at(last + 1).filepath) ++last; MassReplaceJob* job = new MassReplaceJob(searchResults.mid(i, last + 1 - i), i, what, with); QObject::connect(job, &MassReplaceJob::done, this, &FileSearchTab::replaceJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } static void copy(QTreeView* view, int column) { QApplication::clipboard()->setText(view->currentIndex().sibling(view->currentIndex().row(), column).data().toString()); } void FileSearchTab::copySourceToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Source); } void FileSearchTab::copyTargetToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Target); } void FileSearchTab::openFile() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); SearchResult sr = m_model->searchResult(item); DocPosition docPos = sr.docPos.toDocPosition(); int selection = 0; if (sr.targetPositions.size()) { docPos.offset = sr.targetPositions.first().start; selection = sr.targetPositions.first().len; } qCDebug(LOKALIZE_LOG) << "fileOpenRequest" << docPos.offset << selection; emit fileOpenRequested(sr.filepath, docPos, selection, true); } void FileSearchTab::fileSearchNext() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); int row = item.row(); int rowCount = m_model->rowCount(); if (++row >= rowCount) //ok if row was -1 (no solection) return; ui_fileSearchOptions->treeView->setCurrentIndex(item.sibling(row, item.column())); openFile(); } QStringList scanRecursive(const QList& urls) { QStringList result; int i = urls.size(); while (--i >= 0) { if (urls.at(i).isEmpty() || urls.at(i).path().isEmpty()) //NOTE is this a Qt bug? continue; QString path = urls.at(i).toLocalFile(); if (Catalog::extIsSupported(path)) result.append(path); else result += doScanRecursive(QDir(path)); } return result; } //returns gross number of jobs started static QStringList doScanRecursive(const QDir& dir) { QStringList result; QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) result += doScanRecursive(QDir(dir.filePath(subDirs.at(i)))); QStringList filters = Catalog::supportedExtensions(); i = filters.size(); while (--i >= 0) filters[i].prepend('*'); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); while (--i >= 0) result.append(dir.filePath(files.at(i))); return result; } void FileSearchTab::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void FileSearchTab::dropEvent(QDropEvent *event) { event->acceptProposedAction(); addFilesToSearch(scanRecursive(event->mimeData()->urls())); } void FileSearchTab::addFilesToSearch(const QStringList& files) { m_searchFileListView->addFiles(files); performSearch(); } void FileSearchTab::setSourceQuery(const QString& query) { ui_fileSearchOptions->querySource->setText(query); } void FileSearchTab::setTargetQuery(const QString& query) { ui_fileSearchOptions->queryTarget->setText(query); } void FileSearchTab::searchJobDone(SearchJob* j) { j->deleteLater(); if (j->searchNumber != m_lastSearchNumber) return; /* SearchResults searchResults; FileSearchResults::const_iterator i = j->results.constBegin(); while (i != j->results.constEnd()) { foreach(const FileSearchResult& fsr, i.value()) { SearchResult sr(fsr); sr.filepath=i.key(); searchResults<appendSearchResults(searchResults); */ if (j->results.size()) { m_model->appendSearchResults(j->results); m_searchFileListView->scrollTo(j->results.last().filepath); } statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1", m_model->rowCount())); //ui_fileSearchOptions->treeView->setFocus(); } void FileSearchTab::replaceJobDone(MassReplaceJob* j) { j->deleteLater(); ui_fileSearchOptions->treeView->scrollTo(m_model->index(j->globalPos + j->searchResults.count(), 0)); } //END FileSearchTab //BEGIN MASS REPLACE MassReplaceView::MassReplaceView(QWidget* parent) : QDockWidget(i18nc("@title:window", "Mass replace"), parent) , ui(new Ui_MassReplaceOptions) { QWidget* base = new QWidget(this); setWidget(base); ui->setupUi(base); connect(ui->doPreview, &QPushButton::toggled, this, &MassReplaceView::requestPreview); connect(ui->doReplace, &QPushButton::clicked, this, &MassReplaceView::requestReplace); /* QLabel* rl=new QLabel(i18n("Replace:"), base); QLineEdit* searchEdit=new QLineEdit(base); QHBoxLayout* searchL=new QHBoxLayout(); searchL->addWidget(rl); searchL->addWidget(searchEdit); QLabel* wl=new QLabel(i18n("With:"), base); wl->setAlignment(Qt::AlignRight); wl->setMinimumSize(rl->minimumSizeHint()); QLineEdit* replacementEdit=new QLineEdit(base); QHBoxLayout* replacementL=new QHBoxLayout(); replacementL->addWidget(wl); replacementL->addWidget(replacementEdit); FlowLayout* fl=new FlowLayout(); fl->addItem(searchL); fl->addItem(replacementL); base->setLayout(fl); */ } MassReplaceView::~MassReplaceView() { delete ui; } static QRegExp regExpFromUi(const QString& s, Ui_MassReplaceOptions* ui) { return QRegExp(s, ui->matchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive, ui->useRegExps->isChecked() ? QRegExp::RegExp : QRegExp::FixedString); } void MassReplaceView::requestPreviewUpdate() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.length()) ui->doReplace->setEnabled(true); emit previewRequested(regExpFromUi(s, ui), r); } void MassReplaceView::requestPreview(bool enable) { if (enable) { connect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); connect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); requestPreviewUpdate(); } else { disconnect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); emit previewRequested(QRegExp(), QString()); } } void MassReplaceView::requestReplace() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.isEmpty()) return; emit replaceRequested(regExpFromUi(s, ui), r); } void MassReplaceView::deactivatePreview() { ui->doPreview->setChecked(false); ui->doReplace->setEnabled(false); } #include "filesearchadaptor.h" #include QList FileSearchTab::ids; //BEGIN DBus interface QString FileSearchTab::dbusObjectPath() { QString FILESEARCH_PATH = QStringLiteral("/ThisIsWhatYouWant/FileSearch/"); if (m_dbusId == -1) { new FileSearchAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(FILESEARCH_PATH + QString::number(m_dbusId), this); } return FILESEARCH_PATH + QString::number(m_dbusId); } bool FileSearchTab::findGuiTextPackage(QString text, QString package) { setSourceQuery(text); performSearch(); return true; } //END DBus interface diff --git a/src/glossary/glossary.cpp b/src/glossary/glossary.cpp index cd8d887..f1655be 100644 --- a/src/glossary/glossary.cpp +++ b/src/glossary/glossary.cpp @@ -1,734 +1,733 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "glossary.h" #include "lokalize_debug.h" #include "stemming.h" // #include "tbxparser.h" #include "project.h" #include "prefs_lokalize.h" #include "domroutines.h" #include -#include +#include #include #include #include #include #include -#include #include using namespace GlossaryNS; static const QString defaultLang = QStringLiteral("en_US"); static const QString xmlLang = QStringLiteral("xml:lang"); static const QString ntig = QStringLiteral("ntig"); static const QString tig = QStringLiteral("tig"); static const QString termGrp = QStringLiteral("termGrp"); static const QString langSet = QStringLiteral("langSet"); static const QString term = QStringLiteral("term"); static const QString id = QStringLiteral("id"); QList Glossary::idsForLangWord(const QString& lang, const QString& word) const { return idsByLangWord[lang].values(word); } Glossary::Glossary(QObject* parent) : QObject(parent) , m_clean(true) { } //BEGIN DISK bool Glossary::load(const QString& newPath) { QElapsedTimer a; a.start(); //BEGIN NEW QIODevice* device = new QFile(newPath); if (!device->open(QFile::ReadOnly | QFile::Text)) { delete device; //return; device = new QBuffer(); static_cast(device)->setData(QByteArray( "\n" "\n" "\n" " \n" " \n" " \n" " \n" "\n" )); } QXmlSimpleReader reader; reader.setFeature("http://qt-project.org/xml/features/report-whitespace-only-CharData",true); reader.setFeature("http://xml.org/sax/features/namespaces", false); QXmlInputSource source(device); QDomDocument newDoc; QString errorMsg; int errorLine;//+errorColumn; bool success = newDoc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); delete device; if (!success) { qCWarning(LOKALIZE_LOG) << errorMsg; return false; //errorLine+1; } clear();//does setClean(true); m_path = newPath; m_doc = newDoc; //QDomElement file=m_doc.elementsByTagName("file").at(0).toElement(); m_entries = m_doc.elementsByTagName(QStringLiteral("termEntry")); for (int i = 0; i < m_entries.size(); i++) hashTermEntry(m_entries.at(i).toElement()); m_idsForEntriesById = m_entriesById.keys(); //END NEW #if 0 TbxParser parser(this); QXmlSimpleReader reader1; reader1.setContentHandler(&parser); QFile file(p); if (!file.open(QFile::ReadOnly | QFile::Text)) return; QXmlInputSource xmlInputSource(&file); if (!reader1.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load " << path; #endif emit loaded(); if (a.elapsed() > 50) qCDebug(LOKALIZE_LOG) << "glossary loaded in" << a.elapsed(); return true; } bool Glossary::save() { if (m_path.isEmpty()) return false; QFile* device = new QFile(m_path); if (!device->open(QFile::WriteOnly | QFile::Truncate)) { device->deleteLater(); return false; } QTextStream stream(device); m_doc.save(stream, 2); device->deleteLater(); setClean(true); return true; } void Glossary::setClean(bool clean) { m_clean = clean; emit changed();//may be emitted multiple times in a row. so what? :) } //END DISK //BEGIN MODEL #define FETCH_SIZE 128 void GlossarySortFilterProxyModel::setFilterRegExp(const QString& s) { if (!sourceModel()) return; //static const QRegExp lettersOnly("^[a-z]"); QSortFilterProxyModel::setFilterRegExp(s); fetchMore(QModelIndex()); } void GlossarySortFilterProxyModel::fetchMore(const QModelIndex&) { int expectedCount = rowCount() + FETCH_SIZE / 2; while (rowCount(QModelIndex()) < expectedCount && sourceModel()->canFetchMore(QModelIndex())) { sourceModel()->fetchMore(QModelIndex()); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); } } GlossaryModel::GlossaryModel(QObject* parent) : QAbstractListModel(parent) , m_visibleCount(0) , m_glossary(Project::instance()->glossary()) { connect(m_glossary, &Glossary::loaded, this, &GlossaryModel::forceReset); } void GlossaryModel::forceReset() { beginResetModel(); m_visibleCount = 0; endResetModel(); } bool GlossaryModel::canFetchMore(const QModelIndex&) const { return false;//!parent.isValid() && m_glossary->size()!=m_visibleCount; } void GlossaryModel::fetchMore(const QModelIndex& parent) { int newVisibleCount = qMin(m_visibleCount + FETCH_SIZE, m_glossary->size()); beginInsertRows(parent, m_visibleCount, newVisibleCount - 1); m_visibleCount = newVisibleCount; endInsertRows(); } int GlossaryModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_glossary->size();//m_visibleCount; } QVariant GlossaryModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { //case ID: return i18nc("@title:column","ID"); case English: return i18nc("@title:column Original text", "Source");; case Target: return i18nc("@title:column Text in target language", "Target"); case SubjectField: return i18nc("@title:column", "Subject Field"); } return QVariant(); } QVariant GlossaryModel::data(const QModelIndex& index, int role) const { //if (role==Qt::SizeHintRole) // return QVariant(QSize(50, 30)); if (role != Qt::DisplayRole) return QVariant(); static const QString nl = QStringLiteral(" ") + QChar(0x00B7) + ' '; static Project* project = Project::instance(); Glossary* glossary = m_glossary; QByteArray id = glossary->id(index.row()); switch (index.column()) { case ID: return id; case English: return glossary->terms(id, project->sourceLangCode()).join(nl); case Target: return glossary->terms(id, project->targetLangCode()).join(nl); case SubjectField: return glossary->subjectField(id); } return QVariant(); } /* QModelIndex GlossaryModel::index (int row,int column,const QModelIndex& parent) const { return createIndex (row, column); } */ int GlossaryModel::columnCount(const QModelIndex&) const { return GlossaryModelColumnCount; } /* Qt::ItemFlags GlossaryModel::flags ( const QModelIndex & index ) const { return Qt::ItemIsSelectable|Qt::ItemIsEnabled; //if (index.column()==FuzzyFlag) // return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; //return QAbstractItemModel::flags(index); } */ //END MODEL general (GlossaryModel continues below) //BEGIN EDITING QByteArray Glossary::generateNewId() { // generate unique ID int idNumber = 0; QList busyIdNumbers; QString authorId(Settings::authorName().toLower()); authorId.replace(' ', '_'); QRegExp rx('^' + authorId + QStringLiteral("\\-([0-9]*)$")); foreach (const QByteArray& id, m_idsForEntriesById) { if (rx.exactMatch(QString::fromLatin1(id))) busyIdNumbers.append(rx.cap(1).toInt()); } int i = removedIds.size(); while (--i >= 0) { if (rx.exactMatch(QString::fromLatin1(removedIds.at(i)))) busyIdNumbers.append(rx.cap(1).toInt()); } if (!busyIdNumbers.isEmpty()) { std::sort(busyIdNumbers.begin(), busyIdNumbers.end()); while (busyIdNumbers.contains(idNumber)) ++idNumber; } return authorId.toLatin1() + '-' + QByteArray::number(idNumber); } QStringList Glossary::subjectFields() const { QSet result; foreach (const QByteArray& id, m_idsForEntriesById) result.insert(subjectField(id)); return result.values(); } QByteArray Glossary::id(int index) const { if (index < m_idsForEntriesById.size()) return m_idsForEntriesById.at(index); return QByteArray(); } QStringList Glossary::terms(const QByteArray& id, const QString& language) const { QString minusLang = language; minusLang.replace('_', '-'); QStringRef soleLang = language.leftRef(2); QStringList result; QDomElement n = m_entriesById.value(id).firstChildElement(langSet); while (!n.isNull()) { QString lang = n.attribute(xmlLang, defaultLang); if (language == lang || minusLang == lang || soleLang == lang) { QDomElement ntigElem = n.firstChildElement(ntig); while (!ntigElem.isNull()) { result << ntigElem.firstChildElement(termGrp).firstChildElement(term).text(); ntigElem = ntigElem.nextSiblingElement(ntig); } QDomElement tigElem = n.firstChildElement(tig); while (!tigElem.isNull()) { result << tigElem.firstChildElement(term).text(); tigElem = tigElem.nextSiblingElement(tig); } } n = n.nextSiblingElement(langSet); } return result; } // QDomElement ourLangSetElement will reference the lang tag we want (if it exists) static void getElementsForTermLangIndex(QDomElement termEntry, QString& lang, int index, QDomElement& ourLangSetElement, QDomElement& tigElement, //<-- can point to as well QDomElement& termElement) { QString minusLang = lang; minusLang.replace('_', '-'); QStringRef soleLang = lang.leftRef(2); QDomElement n = termEntry.firstChildElement(langSet); QDomDocument document = n.ownerDocument(); int i = 0; while (!n.isNull()) { QString nLang = n.attribute(xmlLang, defaultLang); if (lang == nLang || minusLang == nLang || soleLang == nLang) { ourLangSetElement = n; QDomElement ntigElem = n.firstChildElement(ntig); while (!ntigElem.isNull()) { if (i == index) { tigElement = ntigElem; termElement = ntigElem.firstChildElement(termGrp).firstChildElement(term); return; } ntigElem = ntigElem.nextSiblingElement(ntig); i++; } QDomElement tigElem = n.firstChildElement(tig); while (!tigElem.isNull()) { //qCDebug(LOKALIZE_LOG)<& wordHash, // const QString& what, // int index) void Glossary::hashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); if (entryId.isEmpty()) return; m_entriesById.insert(entryId, termEntry); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].insert(stem(sourceLangCode, word), entryId); } } void Glossary::unhashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); m_entriesById.remove(entryId); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].remove(stem(sourceLangCode, word), entryId); } } #if 0 void Glossary::hashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.insert(stem(Project::instance()->sourceLangCode(), word), index); } } void Glossary::unhashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.remove(stem(Project::instance()->sourceLangCode(), word), index); } } #endif void Glossary::removeEntry(const QByteArray& id) { if (!m_entriesById.contains(id)) return; QDomElement entry = m_entriesById.value(id); if (entry.nextSibling().isCharacterData()) entry.parentNode().removeChild(entry.nextSibling()); //nice formatting entry.parentNode().removeChild(entry); m_entriesById.remove(id); unhashTermEntry(entry); m_idsForEntriesById = m_entriesById.keys(); removedIds.append(id); //for new id generation goodness setClean(false); } static void appendTerm(QDomElement langSetElem, const QString& termText) { QDomDocument doc = langSetElem.ownerDocument(); /* QDomElement ntigElement=doc.createElement(ntig); langSetElem.appendChild(ntigElement); QDomElement termGrpElement=doc.createElement(termGrp); ntigElement.appendChild(termGrpElement); QDomElement termElement=doc.createElement(term); termGrpElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); */ QDomElement tigElement = doc.createElement(tig); langSetElem.appendChild(tigElement); QDomElement termElement = doc.createElement(term); tigElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); } QByteArray Glossary::append(const QStringList& sourceTerms, const QStringList& targetTerms) { if (!m_doc.elementsByTagName(QStringLiteral("body")).count()) return QByteArray(); setClean(false); QDomElement termEntry = m_doc.createElement(QStringLiteral("termEntry")); m_doc.elementsByTagName(QStringLiteral("body")).at(0).appendChild(termEntry); //m_entries=m_doc.elementsByTagName("termEntry"); QByteArray newId = generateNewId(); termEntry.setAttribute(::id, QString::fromLatin1(newId)); QDomElement sourceElem = m_doc.createElement(langSet); termEntry.appendChild(sourceElem); sourceElem.setAttribute(xmlLang, Project::instance()->sourceLangCode().replace('_', '-')); foreach (QString sourceTerm, sourceTerms) appendTerm(sourceElem, sourceTerm); QDomElement targetElem = m_doc.createElement(langSet); termEntry.appendChild(targetElem); targetElem.setAttribute(xmlLang, Project::instance()->targetLangCode().replace('_', '-')); foreach (QString targetTerm, targetTerms) appendTerm(targetElem, targetTerm); hashTermEntry(termEntry); m_idsForEntriesById = m_entriesById.keys(); return newId; } void Glossary::append(const QString& _english, const QString& _target) { append(QStringList(_english), QStringList(_target)); } void Glossary::clear() { setClean(true); //path.clear(); idsByLangWord.clear(); m_entriesById.clear(); m_idsForEntriesById.clear(); removedIds.clear(); changedIds_.clear(); addedIds_.clear(); wordHash_.clear(); termList_.clear(); langWordEntry_.clear(); subjectFields_ = QStringList(QString()); m_doc.clear(); } bool GlossaryModel::removeRows(int row, int count, const QModelIndex& parent) { beginRemoveRows(parent, row, row + count - 1); Glossary* glossary = Project::instance()->glossary(); int i = row + count; while (--i >= row) glossary->removeEntry(glossary->id(i)); endRemoveRows(); return true; } // bool GlossaryModel::insertRows(int row,int count,const QModelIndex& parent) // { // if (row!=rowCount()) // return false; QByteArray GlossaryModel::appendRow(const QString& _english, const QString& _target) { bool notify = !canFetchMore(QModelIndex()); if (notify) beginInsertRows(QModelIndex(), rowCount(), rowCount()); QByteArray id = m_glossary->append(QStringList(_english), QStringList(_target)); if (notify) { m_visibleCount++; endInsertRows(); } return id; } //END EDITING diff --git a/src/glossary/glossaryview.cpp b/src/glossary/glossaryview.cpp index a7ba9cd..19bdf14 100644 --- a/src/glossary/glossaryview.cpp +++ b/src/glossary/glossaryview.cpp @@ -1,210 +1,209 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff 2018-2019 by Simon Depiets 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "glossaryview.h" #include "lokalize_debug.h" #include "glossary.h" #include "project.h" #include "catalog.h" #include "flowlayout.h" #include "glossarywindow.h" #include "stemming.h" #include #include #include #include #include #include #include #include using namespace GlossaryNS; GlossaryView::GlossaryView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18nc("@title:window", "Glossary"), parent) , m_browser(new QScrollArea(this)) , m_catalog(catalog) , m_flowLayout(new FlowLayout(FlowLayout::glossary,/*who gets signals*/this, actions, 0, 10)) , m_glossary(Project::instance()->glossary()) , m_rxClean(Project::instance()->markup() + '|' + Project::instance()->accel()) //cleaning regexp; NOTE isEmpty()? , m_rxSplit(QStringLiteral("\\W|\\d"))//splitting regexp , m_currentIndex(-1) , m_normTitle(i18nc("@title:window", "Glossary")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) { setObjectName(QStringLiteral("glossaryView")); QWidget* w = new QWidget(m_browser); m_browser->setWidget(w); m_browser->setWidgetResizable(true); w->setLayout(m_flowLayout); w->show(); setToolTip(i18nc("@info:tooltip", "

Translations for common terms appear here.

" "

Press shortcut displayed near the term to insert its translation.

" "

Use context menu to add new entry (tip: select words in original and translation fields before calling Define new term).

")); setWidget(m_browser); m_browser->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); m_browser->setAutoFillBackground(true); m_browser->setBackgroundRole(QPalette::Window); m_rxClean.setMinimal(true); connect(m_glossary, &Glossary::changed, this, QOverload<>::of(&GlossaryView::slotNewEntryDisplayed), Qt::QueuedConnection); } GlossaryView::~GlossaryView() { } //TODO define new term by dragging some text. // void GlossaryView::dragEnterEvent(QDragEnterEvent* event) // { // /* if(event->mimeData()->hasUrls() && event->mimeData()->urls().first().path().endsWith(".po")) // { // event->acceptProposedAction(); // };*/ // } // // void GlossaryView::dropEvent(QDropEvent *event) // { // event->acceptProposedAction();*/ // } void GlossaryView::slotNewEntryDisplayed() { slotNewEntryDisplayed(DocPosition()); } void GlossaryView::slotNewEntryDisplayed(DocPosition pos) { //qCWarning(LOKALIZE_LOG)<<"\n\n\n\nstart"<numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection //if (!toggleViewAction()->isChecked()) // return; Glossary& glossary = *m_glossary; QString source = m_catalog->source(pos); QString sourceLowered = source.toLower(); QString msg = sourceLowered; msg.remove(m_rxClean); QString msgStemmed; // QRegExp accel(Project::instance()->accel()); // qCWarning(LOKALIZE_LOG)<accel()<sourceLangCode(); QList termIds; foreach (const QString& w, msg.split(m_rxSplit, QString::SkipEmptyParts)) { QString word = stem(sourceLangCode, w); QList indexes = glossary.idsForLangWord(sourceLangCode, word); //if (indexes.size()) //qCWarning(LOKALIZE_LOG)<<"found entry for:" <clearTerms(); bool found = false; //m_flowLayout->setEnabled(false); foreach (const QByteArray& termId, termIds.toSet()) { // now check which of them are really hits... foreach (const QString& enTerm, glossary.terms(termId, sourceLangCode)) { // ...and if so, which part of termEn list we must thank for match ... bool ok = msg.contains(enTerm); //,//Qt::CaseInsensitive //we lowered terms on load if (!ok) { QString enTermStemmed; foreach (const QString& word, enTerm.split(m_rxSplit, QString::SkipEmptyParts)) enTermStemmed += stem(sourceLangCode, word) + ' '; ok = msgStemmed.contains(enTermStemmed); } if (ok) { //insert it into label found = true; int pos = sourceLowered.indexOf(enTerm); m_flowLayout->addTerm(enTerm, termId,/*uppercase*/pos != -1 && source.at(pos).isUpper()); break; } } } //m_flowLayout->setEnabled(true); if (!found) clear(); else if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(true); } void GlossaryView::clear() { if (m_hasInfo) { m_flowLayout->clearTerms(); m_hasInfo = false; setWindowTitle(m_normTitle); } } diff --git a/src/glossary/glossarywindow.cpp b/src/glossary/glossarywindow.cpp index a699169..0b8e931 100644 --- a/src/glossary/glossarywindow.cpp +++ b/src/glossary/glossarywindow.cpp @@ -1,560 +1,560 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "glossarywindow.h" #include "lokalize_debug.h" #include "glossary.h" #include "project.h" #include "languagelistmodel.h" #include "ui_termedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace GlossaryNS; //BEGIN GlossaryTreeView GlossaryTreeView::GlossaryTreeView(QWidget *parent) : QTreeView(parent) { setSortingEnabled(true); sortByColumn(GlossaryModel::English, Qt::AscendingOrder); setItemsExpandable(false); setAllColumnsShowFocus(true); /* setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows);*/ } static QByteArray modelIndexToId(const QModelIndex& item) { return item.sibling(item.row(), 0).data(Qt::DisplayRole).toByteArray(); } void GlossaryTreeView::currentChanged(const QModelIndex& current, const QModelIndex&/* previous*/) { if (current.isValid()) { //QModelIndex item=static_cast(model())->mapToSource(current); //emit currentChanged(item.row()); emit currentChanged(modelIndexToId(current)); scrollTo(current); } } void GlossaryTreeView::selectRow(int i) { QSortFilterProxyModel* proxyModel = static_cast(model()); GlossaryModel* sourceModel = static_cast(proxyModel->sourceModel()); //sourceModel->forceReset(); setCurrentIndex(proxyModel->mapFromSource(sourceModel->index(i, 0))); } //END GlossaryTreeView //BEGIN SubjectFieldModel //typedef QStringListModel SubjectFieldModel; #if 0 class SubjectFieldModel: public QAbstractItemModel { public: //Q_OBJECT SubjectFieldModel(QObject* parent); ~SubjectFieldModel() {} QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; QModelIndex parent(const QModelIndex&) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; int columnCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const; bool setData(const QModelIndex&, const QVariant&, int role = Qt::EditRole); bool setItemData(const QModelIndex& index, const QMap& roles); bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()); Qt::ItemFlags flags(const QModelIndex&) const; /*private: Catalog* m_catalog;*/ }; inline SubjectFieldModel::SubjectFieldModel(QObject* parent) : QAbstractItemModel(parent) // , m_catalog(catalog) { } QModelIndex SubjectFieldModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } Qt::ItemFlags SubjectFieldModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled; } QModelIndex SubjectFieldModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int SubjectFieldModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return 1; } /* inline Qt::ItemFlags SubjectFieldModel::flags ( const QModelIndex & index ) const { if (index.column()==FuzzyFlag) return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; return QAbstractItemModel::flags(index); }*/ int SubjectFieldModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return Project::instance()->glossary()->subjectFields.size(); } QVariant SubjectFieldModel::data(const QModelIndex& index, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole) return Project::instance()->glossary()->subjectFields.at(index.row()); return QVariant(); } bool SubjectFieldModel::insertRows(int row, int count, const QModelIndex& parent) { beginInsertRows(parent, row, row + count - 1); QStringList& subjectFields = Project::instance()->glossary()->subjectFields; while (--count >= 0) subjectFields.insert(row + count, QString()); endInsertRows(); return true; } bool SubjectFieldModel::setData(const QModelIndex& index, const QVariant& value, int role) { QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = value.toString(); return true; } bool SubjectFieldModel::setItemData(const QModelIndex& index, const QMap& roles) { if (roles.contains(Qt::EditRole)) { QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = roles.value(Qt::EditRole).toString(); } return true; } #endif //END SubjectFieldModel //BEGIN GlossaryWindow GlossaryWindow::GlossaryWindow(QWidget *parent) : KMainWindow(parent) , m_browser(new GlossaryTreeView(this)) , m_proxyModel(new GlossarySortFilterProxyModel(this)) , m_reactOnSignals(true) { //setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, false); QSplitter* splitter = new QSplitter(Qt::Horizontal, this); setCentralWidget(splitter); m_proxyModel->setFilterKeyColumn(-1); m_proxyModel->setDynamicSortFilter(true);; GlossaryModel* model = new GlossaryModel(this); m_proxyModel->setSourceModel(model); m_browser->setModel(m_proxyModel); m_browser->setUniformRowHeights(true); m_browser->setAutoScroll(true); m_browser->setColumnHidden(GlossaryModel::ID, true); m_browser->setColumnWidth(GlossaryModel::English, m_browser->columnWidth(GlossaryModel::English) * 2); //man this is HACK y m_browser->setColumnWidth(GlossaryModel::Target, m_browser->columnWidth(GlossaryModel::Target) * 2); m_browser->setAlternatingRowColors(true); //left QWidget* w = new QWidget(splitter); QVBoxLayout* layout = new QVBoxLayout(w); m_filterEdit = new QLineEdit(w); m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setPlaceholderText(i18n("Quick search...")); m_filterEdit->setFocus(); m_filterEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); new QShortcut(Qt::CTRL + Qt::Key_L, this, SLOT(setFocus()), 0, Qt::WidgetWithChildrenShortcut); connect(m_filterEdit, &QLineEdit::textChanged, m_proxyModel, &GlossaryNS::GlossarySortFilterProxyModel::setFilterRegExp); layout->addWidget(m_filterEdit); layout->addWidget(m_browser); { QPushButton* addBtn = new QPushButton(w); connect(addBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::newTermEntry)); QPushButton* rmBtn = new QPushButton(w); connect(rmBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::rmTermEntry)); KGuiItem::assign(addBtn, KStandardGuiItem::add()); KGuiItem::assign(rmBtn, KStandardGuiItem::remove()); QPushButton* restoreBtn = new QPushButton(i18nc("@action:button reloads glossary from disk", "Restore from disk"), w); restoreBtn->setToolTip(i18nc("@info:tooltip", "Reload glossary from disk, discarding any changes")); connect(restoreBtn, &QPushButton::clicked, this, &GlossaryWindow::restore); QWidget* btns = new QWidget(w); QHBoxLayout* btnsLayout = new QHBoxLayout(btns); btnsLayout->addWidget(addBtn); btnsLayout->addWidget(rmBtn); btnsLayout->addWidget(restoreBtn); layout->addWidget(btns); //QWidget::setTabOrder(m_browser,addBtn); QWidget::setTabOrder(addBtn, rmBtn); QWidget::setTabOrder(rmBtn, restoreBtn); QWidget::setTabOrder(restoreBtn, m_filterEdit); } QWidget::setTabOrder(m_filterEdit, m_browser); splitter->addWidget(w); //right m_editor = new QWidget(splitter); m_editor->hide(); Ui_TermEdit ui_termEdit; ui_termEdit.setupUi(m_editor); splitter->addWidget(m_editor); Project* project = Project::instance(); m_sourceTermsModel = new TermsListModel(project->glossary(), project->sourceLangCode(), this); m_targetTermsModel = new TermsListModel(project->glossary(), project->targetLangCode(), this); ui_termEdit.sourceTermsView->setModel(m_sourceTermsModel); ui_termEdit.targetTermsView->setModel(m_targetTermsModel); connect(ui_termEdit.addEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::addTerm); connect(ui_termEdit.remEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::rmTerms); connect(ui_termEdit.addTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::addTerm); connect(ui_termEdit.remTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::rmTerms); m_sourceTermsView = ui_termEdit.sourceTermsView; m_targetTermsView = ui_termEdit.targetTermsView; m_subjectField = ui_termEdit.subjectField; m_definition = ui_termEdit.definition; m_definitionLang = ui_termEdit.definitionLang; //connect (m_english,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_target,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(editingFinished()),this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(textChanged()),this,SLOT(applyEntryChange())); //connect (m_subjectField,SIGNAL(editTextChanged(QString)),this,SLOT(applyEntryChange())); connect(m_subjectField->lineEdit(), &QLineEdit::editingFinished, this, &GlossaryWindow::applyEntryChange); //m_subjectField->addItems(Project::instance()->glossary()->subjectFields()); //m_subjectField->setModel(new SubjectFieldModel(this)); QStringList subjectFields = Project::instance()->glossary()->subjectFields(); std::sort(subjectFields.begin(), subjectFields.end()); QStringListModel* subjectFieldsModel = new QStringListModel(this); subjectFieldsModel->setStringList(subjectFields); m_subjectField->setModel(subjectFieldsModel); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::currentChanged); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::showEntryInEditor); connect(m_definitionLang, QOverload::of(&KComboBox::activated), this, &GlossaryWindow::showDefinitionForLang); m_definitionLang->setModel(LanguageListModel::emptyLangInstance()->sortModel()); m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(m_defLang));//empty lang //TODO //connect(m_targetTermsModel,SIGNAL(dataChanged(QModelIndex,QModelIndex)),m_browser,SLOT(setFocus())); setAutoSaveSettings(QLatin1String("GlossaryWindow"), true); //Glossary* glossary=Project::instance()->glossary(); /*setCaption(i18nc("@title:window","Glossary"), !glossary->changedIds.isEmpty()||!glossary->addedIds.isEmpty()||!glossary->removedIds.isEmpty()); */ } void GlossaryWindow::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void GlossaryWindow::showEntryInEditor(const QByteArray& id) { if (m_editor->isVisible()) applyEntryChange(); else m_editor->show(); m_id = id; m_reactOnSignals = false; Project* project = Project::instance(); Glossary* glossary = project->glossary(); m_subjectField->setCurrentItem(glossary->subjectField(id),/*insert*/true); QStringList langsToTry = QStringList(m_defLang) << QStringLiteral("en") << QStringLiteral("en_US") << project->targetLangCode(); foreach (const QString& lang, langsToTry) { QString d = glossary->definition(m_id, lang); if (!d.isEmpty()) { if (m_defLang != lang) m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(lang)); m_defLang = lang; break; } } m_definition->setPlainText(glossary->definition(m_id, m_defLang)); m_sourceTermsModel->setEntry(id); m_targetTermsModel->setEntry(id); //m_sourceTermsModel->setStringList(glossary->terms(id,project->sourceLangCode())); //m_targetTermsModel->setStringList(glossary->terms(id,project->targetLangCode())); m_reactOnSignals = true; } void GlossaryWindow::currentChanged(int i) { Q_UNUSED(i); m_reactOnSignals = false; m_editor->show(); m_reactOnSignals = true; } void GlossaryWindow::showDefinitionForLang(int langModelIndex) { applyEntryChange(); m_defLang = LanguageListModel::emptyLangInstance()->langCodeForSortModelRow(langModelIndex); m_definition->setPlainText(Project::instance()->glossary()->definition(m_id, m_defLang)); } void GlossaryWindow::applyEntryChange() { if (!m_reactOnSignals || !m_browser->currentIndex().isValid()) return; QByteArray id = m_id; //modelIndexToId(m_browser->currentIndex()); Project* project = Project::instance(); Glossary* glossary = project->glossary(); if (m_subjectField->currentText() != glossary->subjectField(id)) glossary->setSubjectField(id, QString(), m_subjectField->currentText()); if (m_definition->toPlainText() != glossary->definition(id, m_defLang)) glossary->setDefinition(id, m_defLang, m_definition->toPlainText()); //HACK to force finishing of the listview editing QWidget* prevFocusWidget = QApplication::focusWidget(); m_browser->setFocus(); if (prevFocusWidget) prevFocusWidget->setFocus(); // QSortFilterProxyModel* proxyModel=static_cast(model()); //GlossaryModel* sourceModel=static_cast(m_proxyModel->sourceModel()); const QModelIndex& idx = m_proxyModel->mapToSource(m_browser->currentIndex()); if (!idx.isValid()) return; //TODO display filename, optionally stripped like for filetab names setCaption(i18nc("@title:window", "Glossary"), !glossary->isClean()); } void GlossaryWindow::selectEntry(const QByteArray& id) { //let it fetch the rows QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers | QEventLoop::WaitForMoreEvents, 100); - QModelIndexList items = m_proxyModel->match(m_proxyModel->index(0, 0), Qt::DisplayRole, QVariant(id), 1, 0); + QModelIndexList items = m_proxyModel->match(m_proxyModel->index(0, 0), Qt::DisplayRole, QVariant(id), 1, Qt::MatchExactly); if (items.count()) { m_browser->setCurrentIndex(items.first()); m_browser->scrollTo(items.first(), QAbstractItemView::PositionAtCenter); } else { //the row is probably not fetched yet m_browser->setCurrentIndex(QModelIndex()); showEntryInEditor(id); } } void GlossaryWindow::newTermEntry() { newTermEntry(QString(), QString()); } void GlossaryWindow::newTermEntry(QString _english, QString _target) { setCaption(i18nc("@title:window", "Glossary"), true); GlossaryModel* sourceModel = static_cast(m_proxyModel->sourceModel()); QByteArray id = sourceModel->appendRow(_english, _target); selectEntry(id); } void GlossaryWindow::rmTermEntry() { rmTermEntry(-1); } void GlossaryWindow::rmTermEntry(int i) { setCaption(i18nc("@title:window", "Glossary"), true); //QSortFilterProxyModel* proxyModel=static_cast(model()); GlossaryModel* sourceModel = static_cast(m_proxyModel->sourceModel()); if (i == -1) { //NOTE actually we should remove selected items, not current one const QModelIndex& current = m_browser->currentIndex(); if (!current.isValid()) return; i = m_proxyModel->mapToSource(current).row(); } sourceModel->removeRow(i); } void GlossaryWindow::restore() { setCaption(i18nc("@title:window", "Glossary"), false); Glossary* glossary = Project::instance()->glossary(); glossary->load(glossary->path()); m_reactOnSignals = false; showEntryInEditor(m_id); m_reactOnSignals = true; } bool GlossaryWindow::save() { //TODO add error message return Project::instance()->glossary()->save(); } bool GlossaryWindow::queryClose() { Glossary* glossary = Project::instance()->glossary(); applyEntryChange(); if (glossary->isClean()) return true; switch (KMessageBox::warningYesNoCancel(this, i18nc("@info", "The glossary contains unsaved changes.\n\ Do you want to save your changes or discard them?"), i18nc("@title:window", "Warning"), KStandardGuiItem::save(), KStandardGuiItem::discard())) { case KMessageBox::Yes: return save(); case KMessageBox::No: restore(); return true; default: return false; } } //END GlossaryWindow void TermsListModel::setEntry(const QByteArray& id) { m_id = id; QStringList terms = m_glossary->terms(m_id, m_lang); terms.append(QString()); //allow adding new terms setStringList(terms); } bool TermsListModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_UNUSED(role); m_glossary->setTerm(m_id, m_lang, index.row(), value.toString()); setEntry(m_id); //allow adding new terms return true; } bool TermsListModel::removeRows(int row, int count, const QModelIndex& parent) { Q_UNUSED(count) if (row == rowCount() - 1) return false;// cannot delete non-existing item m_glossary->rmTerm(m_id, m_lang, row); return QStringListModel::removeRows(row, 1, parent); } void TermListView::addTerm() { setCurrentIndex(model()->index(model()->rowCount() - 1, 0)); edit(currentIndex()); } void TermListView::rmTerms() { foreach (const QModelIndex& row, selectionModel()->selectedRows()) model()->removeRow(row.row()); } diff --git a/src/lokalizemainwindow.cpp b/src/lokalizemainwindow.cpp index 9df022c..032033a 100644 --- a/src/lokalizemainwindow.cpp +++ b/src/lokalizemainwindow.cpp @@ -1,1087 +1,1087 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-2015 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalizemainwindow.h" #include "lokalize_debug.h" #include "actionproxy.h" #include "editortab.h" #include "projecttab.h" #include "tmtab.h" #include "jobs.h" #include "filesearchtab.h" #include "prefs_lokalize.h" // #define WEBQUERY_ENABLE #include "project.h" #include "projectmodel.h" #include "projectlocal.h" #include "prefs.h" #include "tools/widgettextcaptureconfig.h" #include "multieditoradaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include LokalizeMainWindow::LokalizeMainWindow() : KXmlGuiWindow() , m_mdiArea(new LokalizeMdiArea) , m_prevSubWindow(0) , m_projectSubWindow(0) , m_translationMemorySubWindow(0) , m_editorActions(new QActionGroup(this)) , m_managerActions(new QActionGroup(this)) , m_spareEditor(new EditorTab(this, false)) , m_multiEditorAdaptor(new MultiEditorAdaptor(m_spareEditor)) , m_projectScriptingPlugin(0) { m_spareEditor->hide(); m_mdiArea->setViewMode(QMdiArea::TabbedView); m_mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); m_mdiArea->setDocumentMode(true); m_mdiArea->setTabsMovable(true); m_mdiArea->setTabsClosable(true); setCentralWidget(m_mdiArea); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &LokalizeMainWindow::slotSubWindowActivated); setupActions(); //prevent relayout of dockwidgets m_mdiArea->setOption(QMdiArea::DontMaximizeSubWindowOnActivation, true); connect(Project::instance(), QOverload::of(&Project::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_), Qt::QueuedConnection); connect(Project::instance(), &Project::configChanged, this, &LokalizeMainWindow::projectSettingsChanged); connect(Project::instance(), &Project::closed, this, &LokalizeMainWindow::closeProject); showProjectOverview(); showTranslationMemory(); //fix for #342558 for (int i = ID_STATUS_CURRENT; i <= ID_STATUS_ISFUZZY; i++) { m_statusBarLabels.append(new QLabel()); statusBar()->insertWidget(i, m_statusBarLabels.last(), 2); } setAttribute(Qt::WA_DeleteOnClose, true); if (!qApp->isSessionRestored()) { KConfig config; KConfigGroup stateGroup(&config, "State"); readProperties(stateGroup); } registerDBusAdaptor(); QTimer::singleShot(0, this, &LokalizeMainWindow::initLater); } void LokalizeMainWindow::initLater() { if (!m_prevSubWindow && m_projectSubWindow) slotSubWindowActivated(m_projectSubWindow); if (!Project::instance()->isTmSupported()) { KNotification* notification = new KNotification("NoSqlModulesAvailable", this); notification->setText(i18nc("@info", "No Qt Sql modules were found. Translation memory will not work.")); notification->sendEvent(); } } LokalizeMainWindow::~LokalizeMainWindow() { TM::cancelAllJobs(); KConfig config; KConfigGroup stateGroup(&config, "State"); if (!m_lastEditorState.isEmpty()) { stateGroup.writeEntry("DefaultDockWidgets", m_lastEditorState); } saveProjectState(stateGroup); m_multiEditorAdaptor->deleteLater(); //Disconnect the signals pointing to this MainWindow object QMdiSubWindow* sw; for (int i = 0; i < m_fileToEditor.values().count(); i++) { sw = m_fileToEditor.values().at(i); disconnect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); EditorTab* w = static_cast(sw->widget()); disconnect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); disconnect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); disconnect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); } qCWarning(LOKALIZE_LOG) << "MainWindow destroyed"; } void LokalizeMainWindow::slotSubWindowActivated(QMdiSubWindow* w) { //QTime aaa;aaa.start(); if (!w || m_prevSubWindow == w) return; w->setUpdatesEnabled(true); //QTBUG-23289 if (m_prevSubWindow) { m_prevSubWindow->setUpdatesEnabled(false); LokalizeSubwindowBase* prevEditor = static_cast(m_prevSubWindow->widget()); prevEditor->hideDocks(); guiFactory()->removeClient(prevEditor->guiClient()); prevEditor->statusBarItems.unregisterStatusBar(); if (qobject_cast(prevEditor)) { EditorTab* w = static_cast(prevEditor); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); } /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { disconnect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); } } */ } LokalizeSubwindowBase* editor = static_cast(w->widget()); editor->reloadUpdatedXML(); if (qobject_cast(editor)) { EditorTab* w = static_cast(editor); w->setProperFocus(); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); m_multiEditorAdaptor->setEditorTab(w); // connect(m_multiEditorAdaptor,SIGNAL(srcFileOpenRequested(QString,int)),this,SLOT(showTM())); /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { connect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); }*/ QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), w->currentFilePath()); emit editorActivated(); } else if (w == m_projectSubWindow && m_projectSubWindow) { QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), Project::instance()->path()); } editor->showDocks(); editor->statusBarItems.registerStatusBar(statusBar(), m_statusBarLabels); guiFactory()->addClient(editor->guiClient()); m_prevSubWindow = w; //qCWarning(LOKALIZE_LOG)<<"finished"< editors = m_mdiArea->subWindowList(); int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!qobject_cast(editors.at(i)->widget())) continue; if (!static_cast(editors.at(i)->widget())->queryClose()) return false; } bool ok = Project::instance()->queryCloseForAuxiliaryWindows(); if (ok) { QThreadPool::globalInstance()->clear(); Project::instance()->model()->threadPool()->clear(); } return ok; } EditorTab* LokalizeMainWindow::fileOpen_(QString filePath, const bool setAsActive) { return fileOpen(filePath, 0, setAsActive); } EditorTab* LokalizeMainWindow::fileOpen(QString filePath, int entry, bool setAsActive, const QString& mergeFile, bool silent) { if (filePath.length()) { FileToEditor::const_iterator it = m_fileToEditor.constFind(filePath); if (it != m_fileToEditor.constEnd()) { qCWarning(LOKALIZE_LOG) << "already opened:" << filePath; if (QMdiSubWindow* sw = it.value()) { m_mdiArea->setActiveSubWindow(sw); return static_cast(sw->widget()); } } } QByteArray state = m_lastEditorState; EditorTab* w = new EditorTab(this); QMdiSubWindow* sw = 0; //create QMdiSubWindow BEFORE fileOpen() because it causes some strange QMdiArea behaviour otherwise if (filePath.length()) sw = m_mdiArea->addSubWindow(w); QString suggestedDirPath; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (activeSW && qobject_cast(activeSW->widget())) { QString fp = static_cast(activeSW->widget())->currentFilePath(); if (fp.length()) suggestedDirPath = QFileInfo(fp).absolutePath(); } if (!w->fileOpen(filePath, suggestedDirPath, m_fileToEditor, silent)) { if (sw) { m_mdiArea->removeSubWindow(sw); sw->deleteLater(); } w->deleteLater(); return 0; } filePath = w->currentFilePath(); m_openRecentFileAction->addUrl(QUrl::fromLocalFile(filePath));//(w->currentUrl()); if (!sw) sw = m_mdiArea->addSubWindow(w); w->showMaximized(); sw->showMaximized(); if (!state.isEmpty()) { w->restoreState(QByteArray::fromBase64(state)); m_lastEditorState = state; } else { //Dummy restore to "initialize" widgets w->restoreState(w->saveState()); } if (entry/* || offset*/) w->gotoEntry(DocPosition(entry/*, DocPosition::Target, 0, offset*/)); if (setAsActive) { m_toBeActiveSubWindow = sw; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } else { m_mdiArea->setActiveSubWindow(activeSW); sw->setUpdatesEnabled(false); //QTBUG-23289 } if (!mergeFile.isEmpty()) w->mergeOpen(mergeFile); connect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); connect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); connect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); QStringRef fnSlashed = filePath.midRef(filePath.lastIndexOf('/')); FileToEditor::const_iterator i = m_fileToEditor.constBegin(); while (i != m_fileToEditor.constEnd()) { if (i.key().endsWith(fnSlashed)) { static_cast(i.value()->widget())->setFullPathShown(true); w->setFullPathShown(true); } ++i; } m_fileToEditor.insert(filePath, sw); sw->setAttribute(Qt::WA_DeleteOnClose, true); emit editorAdded(); return w; } void LokalizeMainWindow::resetMultiEditorAdaptor() { m_multiEditorAdaptor->setEditorTab(m_spareEditor); //it will be reparented shortly if there are other editors } void LokalizeMainWindow::editorClosed(QObject* obj) { m_fileToEditor.remove(m_fileToEditor.key(static_cast(obj))); } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, const QString& source, const QString& ctxt, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->findEntryBySourceContext(source, ctxt); return w; } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, DocPosition docPos, int selection, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->gotoEntry(docPos, selection); return w; } QObject* LokalizeMainWindow::projectOverview() { if (!m_projectSubWindow) { ProjectTab* w = new ProjectTab(this); m_projectSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_projectSubWindow->showMaximized(); connect(w, QOverload::of(&ProjectTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); connect(w, QOverload::of(&ProjectTab::projectOpenRequested), this, QOverload::of(&LokalizeMainWindow::openProject)); connect(w, QOverload<>::of(&ProjectTab::projectOpenRequested), this, QOverload<>::of(&LokalizeMainWindow::openProject)); connect(w, QOverload::of(&ProjectTab::searchRequested), this, QOverload::of(&LokalizeMainWindow::addFilesToSearch)); } if (m_mdiArea->currentSubWindow() == m_projectSubWindow) return m_projectSubWindow->widget(); return 0; } void LokalizeMainWindow::showProjectOverview() { projectOverview(); m_mdiArea->setActiveSubWindow(m_projectSubWindow); } TM::TMTab* LokalizeMainWindow::showTM() { if (!Project::instance()->isTmSupported()) { KMessageBox::information(nullptr, i18n("TM facility requires SQLite Qt module."), i18n("No SQLite module available")); return 0; } if (!m_translationMemorySubWindow) { TM::TMTab* w = new TM::TMTab(this); m_translationMemorySubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_translationMemorySubWindow->showMaximized(); connect(w, QOverload::of(&TM::TMTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); } m_mdiArea->setActiveSubWindow(m_translationMemorySubWindow); return static_cast(m_translationMemorySubWindow->widget()); } FileSearchTab* LokalizeMainWindow::showFileSearch(bool activate) { EditorTab* precedingEditor = qobject_cast(activeEditor()); if (!m_fileSearchSubWindow) { FileSearchTab* w = new FileSearchTab(this); m_fileSearchSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_fileSearchSubWindow->showMaximized(); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); } if (activate) { m_mdiArea->setActiveSubWindow(m_fileSearchSubWindow); if (precedingEditor) { if (precedingEditor->selectionInSource().length()) static_cast(m_fileSearchSubWindow->widget())->setSourceQuery(precedingEditor->selectionInSource()); if (precedingEditor->selectionInTarget().length()) static_cast(m_fileSearchSubWindow->widget())->setTargetQuery(precedingEditor->selectionInTarget()); } } return static_cast(m_fileSearchSubWindow->widget()); } void LokalizeMainWindow::fileSearchNext() { FileSearchTab* w = showFileSearch(/*activate=*/false); //TODO fill search params based on current selection w->fileSearchNext(); } void LokalizeMainWindow::addFilesToSearch(const QStringList& files) { FileSearchTab* w = showFileSearch(); w->addFilesToSearch(files); } void LokalizeMainWindow::applyToBeActiveSubWindow() { m_mdiArea->setActiveSubWindow(m_toBeActiveSubWindow); } void LokalizeMainWindow::setupActions() { //all operations that can be done after initial setup //(via QTimer::singleShot) go to initLater() - QElapsedTimer aaa; - aaa.start(); +// QElapsedTimer aaa; +// aaa.start(); setStandardToolBarMenuEnabled(true); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* actionCategory; KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), ac); //KActionCategory* config=new KActionCategory(i18nc("@title actions category","Settings"), ac); KActionCategory* glossary = new KActionCategory(i18nc("@title actions category", "Glossary"), ac); KActionCategory* tm = new KActionCategory(i18nc("@title actions category", "Translation Memory"), ac); KActionCategory* proj = new KActionCategory(i18nc("@title actions category", "Project"), ac); actionCategory = file; // File //KStandardAction::open(this, SLOT(fileOpen()), ac); file->addAction(KStandardAction::Open, this, SLOT(fileOpen())); m_openRecentFileAction = KStandardAction::openRecent(this, SLOT(fileOpen(QUrl)), ac); file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows())); //Settings SettingsController* sc = SettingsController::instance(); KStandardAction::preferences(sc, &SettingsController::showSettingsDialog, ac); #define ADD_ACTION_SHORTCUT(_name,_text,_shortcut)\ action = actionCategory->addAction(QStringLiteral(_name));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut ));\ action->setText(_text); //Window //documentBack //KStandardAction::close(m_mdiArea, SLOT(closeActiveSubWindow()), ac); actionCategory = file; ADD_ACTION_SHORTCUT("next-tab", i18n("Next tab"), Qt::CTRL + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activateNextSubWindow); ADD_ACTION_SHORTCUT("prev-tab", i18n("Previous tab"), Qt::CTRL + Qt::SHIFT + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activatePreviousSubWindow); ADD_ACTION_SHORTCUT("prev-active-tab", i18n("Previously active tab"), Qt::CTRL + Qt::Key_BracketLeft) connect(action, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow); //Tools actionCategory = glossary; Project* project = Project::instance(); ADD_ACTION_SHORTCUT("tools_glossary", i18nc("@action:inmenu", "Glossary"), Qt::CTRL + Qt::ALT + Qt::Key_G) connect(action, &QAction::triggered, project, &Project::showGlossary); actionCategory = tm; ADD_ACTION_SHORTCUT("tools_tm", i18nc("@action:inmenu", "Translation memory"), Qt::Key_F7) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showTM); action = tm->addAction("tools_tm_manage", project, SLOT(showTMManager())); action->setText(i18nc("@action:inmenu", "Manage translation memories")); //Project actionCategory = proj; ADD_ACTION_SHORTCUT("project_overview", i18nc("@action:inmenu", "Project overview"), Qt::Key_F4) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showProjectOverview); action = proj->addAction(QStringLiteral("project_configure"), sc, SLOT(projectConfigure())); action->setText(i18nc("@action:inmenu", "Configure project...")); action = proj->addAction(QStringLiteral("project_create"), sc, SLOT(projectCreate())); action->setText(i18nc("@action:inmenu", "Create software translation project...")); action = proj->addAction(QStringLiteral("project_create_odf"), Project::instance(), SLOT(projectOdfCreate())); action->setText(i18nc("@action:inmenu", "Create OpenDocument translation project...")); action = proj->addAction(QStringLiteral("project_open"), this, SLOT(openProject())); action->setText(i18nc("@action:inmenu", "Open project...")); action->setIcon(QIcon::fromTheme("project-open")); action = proj->addAction(QStringLiteral("project_close"), this, SLOT(closeProject())); action->setText(i18nc("@action:inmenu", "Close project")); action->setIcon(QIcon::fromTheme("project-close")); m_openRecentProjectAction = new KRecentFilesAction(i18nc("@action:inmenu", "Open recent project"), this); action = proj->addAction(QStringLiteral("project_open_recent"), m_openRecentProjectAction); connect(m_openRecentProjectAction, QOverload::of(&KRecentFilesAction::urlSelected), this, QOverload::of(&LokalizeMainWindow::openProject)); //Qt::QueuedConnection: defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash connect(Project::instance(), &Project::loaded, this, &LokalizeMainWindow::projectLoaded, Qt::QueuedConnection); ADD_ACTION_SHORTCUT("tools_filesearch", i18nc("@action:inmenu", "Search and replace in files"), Qt::Key_F6) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showFileSearch); ADD_ACTION_SHORTCUT("tools_filesearch_next", i18nc("@action:inmenu", "Find next in files"), Qt::META + Qt::Key_F3) connect(action, &QAction::triggered, this, &LokalizeMainWindow::fileSearchNext); action = ac->addAction(QStringLiteral("tools_widgettextcapture"), this, SLOT(widgetTextCapture())); action->setText(i18nc("@action:inmenu", "Widget text capture")); setupGUI(Default, QStringLiteral("lokalizemainwindowui.rc")); //qCDebug(LOKALIZE_LOG)<<"action setup finished"<subWindowList()) { if (subwindow == m_translationMemorySubWindow && m_translationMemorySubWindow) subwindow->deleteLater(); else if (qobject_cast(subwindow->widget())) { m_fileToEditor.remove(static_cast(subwindow->widget())->currentFilePath());//safety m_mdiArea->removeSubWindow(subwindow); subwindow->deleteLater(); } else if (subwindow == m_projectSubWindow && m_projectSubWindow) static_cast(m_projectSubWindow->widget())->showWelcomeScreen(); } Project::instance()->load(QString()); //TODO scripts return true; } void LokalizeMainWindow::openProject(QString path) { path = SettingsController::instance()->projectOpen(path, false); //dry run if (path.isEmpty()) return; if (closeProject()) SettingsController::instance()->projectOpen(path, true);//really open } void LokalizeMainWindow::saveProperties(KConfigGroup& stateGroup) { saveProjectState(stateGroup); } void LokalizeMainWindow::saveProjectState(KConfigGroup& stateGroup) { QList editors = m_mdiArea->subWindowList(); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); int activeSWIndex = -1; int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!editors.at(i) || !qobject_cast(editors.at(i)->widget())) continue; EditorState state = static_cast(editors.at(i)->widget())->state(); if (editors.at(i) == activeSW) { activeSWIndex = files.size(); m_lastEditorState = state.dockWidgets.toBase64(); } files.append(state.filePath); mergeFiles.append(state.mergeFilePath); dockWidgets.append(state.dockWidgets.toBase64()); entries.append(state.entry); //offsets.append(state.offset); //qCWarning(LOKALIZE_LOG)<(editors.at(i)->widget() )->state().url; } //if (activeSWIndex==-1 && activeSW==m_projectSubWindow) if (files.size() == 0 && !m_lastEditorState.isEmpty()) { dockWidgets.append(m_lastEditorState); // save last state if no editor open } if (stateGroup.isValid()) stateGroup.writeEntry("Project", Project::instance()->path()); KConfig config; KConfigGroup projectStateGroup(&config, "State-" + Project::instance()->path()); projectStateGroup.writeEntry("Active", activeSWIndex); projectStateGroup.writeEntry("Files", files); projectStateGroup.writeEntry("MergeFiles", mergeFiles); projectStateGroup.writeEntry("DockWidgets", dockWidgets); //stateGroup.writeEntry("Offsets",offsets); projectStateGroup.writeEntry("Entries", entries); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); if (w->unitsCount() > 0) projectStateGroup.writeEntry("UnitsCount", w->unitsCount()); } QString nameSpecifier = Project::instance()->path(); if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); KConfig* c = stateGroup.isValid() ? stateGroup.config() : &config; m_openRecentFileAction->saveEntries(KConfigGroup(c, "RecentFiles" + nameSpecifier)); m_openRecentProjectAction->saveEntries(KConfigGroup(&config, "RecentProjects")); } void LokalizeMainWindow::readProperties(const KConfigGroup& stateGroup) { KConfig config; m_openRecentProjectAction->loadEntries(KConfigGroup(&config, "RecentProjects")); QString path = stateGroup.readEntry("Project", QString()); if (Project::instance()->isLoaded() || path.isEmpty()) { //1. we weren't existing yet when the signal was emitted //2. defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash QTimer::singleShot(0, this, &LokalizeMainWindow::projectLoaded); } else Project::instance()->load(path); } void LokalizeMainWindow::projectLoaded() { QString projectPath = Project::instance()->path(); qCDebug(LOKALIZE_LOG) << "Loaded project : " << projectPath; if (!projectPath.isEmpty()) m_openRecentProjectAction->addUrl(QUrl::fromLocalFile(projectPath)); KConfig config; QString nameSpecifier = projectPath; if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); m_openRecentFileAction->loadEntries(KConfigGroup(&config, "RecentFiles" + nameSpecifier)); //if project isn't loaded, still restore opened files from "State-" KConfigGroup stateGroup(&config, "State"); KConfigGroup projectStateGroup(&config, "State-" + projectPath); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; projectOverview(); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); w->setLegacyUnitsCount(projectStateGroup.readEntry("UnitsCount", 0)); QTabBar* tw = m_mdiArea->findChild(); if (tw) for (int i = 0; i < tw->count(); i++) if (tw->tabText(i) == w->windowTitle()) tw->setTabToolTip(i, Project::instance()->path()); } entries = projectStateGroup.readEntry("Entries", entries); if (Settings::self()->restoreRecentFilesOnStartup()) files = projectStateGroup.readEntry("Files", files); mergeFiles = projectStateGroup.readEntry("MergeFiles", mergeFiles); dockWidgets = projectStateGroup.readEntry("DockWidgets", dockWidgets); int i = files.size(); int activeSWIndex = projectStateGroup.readEntry("Active", -1); QStringList failedFiles; while (--i >= 0) { if (i < dockWidgets.size()) { m_lastEditorState = dockWidgets.at(i); } if (!fileOpen(files.at(i), entries.at(i)/*, offsets.at(i)*//*,&activeSW11*/, activeSWIndex == i, mergeFiles.at(i),/*silent*/true)) failedFiles.append(files.at(i)); } if (!failedFiles.isEmpty()) { qCDebug(LOKALIZE_LOG) << "failedFiles" << failedFiles; // KMessageBox::error(this, i18nc("@info","Error opening the following files:")+ // "
  • "+failedFiles.join("
  • ")+"
  • " ); KNotification* notification = new KNotification("FilesOpenError", this); notification->setText(i18nc("@info", "Error opening the following files:\n\n") + "" + failedFiles.join("
    ") + ""); notification->sendEvent(); } if (files.isEmpty() && dockWidgets.size() > 0) { m_lastEditorState = dockWidgets.last(); // restore last state if no editor open } else { m_lastEditorState = stateGroup.readEntry("DefaultDockWidgets", m_lastEditorState); // restore default state if no last editor for this project } if (activeSWIndex == -1) { m_toBeActiveSubWindow = m_projectSubWindow; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } projectSettingsChanged(); QTimer::singleShot(0, this, &LokalizeMainWindow::loadProjectScripts); } void LokalizeMainWindow::projectSettingsChanged() { //TODO show langs setCaption(Project::instance()->projectID()); } void LokalizeMainWindow::widgetTextCapture() { WidgetTextCaptureConfig* w = new WidgetTextCaptureConfig(this); w->show(); } //BEGIN DBus interface //#include "plugin.h" #include "mainwindowadaptor.h" #include #include using namespace Kross; class MyScriptingPlugin: public Kross::ScriptingPlugin { public: MyScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(lokalize) { addObject(lokalize, "Lokalize"); addObject(Project::instance(), "Project"); addObject(editor, "Editor"); setXMLFile("scriptsui.rc", true); } ~MyScriptingPlugin() {} }; #define PROJECTRCFILE "scripts.rc" #define PROJECTRCFILEDIR Project::instance()->projectDir()+"/lokalize-scripts" #define PROJECTRCFILEPATH Project::instance()->projectDir()+"/lokalize-scripts" "/" PROJECTRCFILE //TODO be lazy creating scripts dir ProjectScriptingPlugin::ProjectScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(Project::instance()->kind(), PROJECTRCFILEPATH, Project::instance()->kind(), lokalize) { if (Project::instance()->projectDir().isEmpty()) return; QString filepath = PROJECTRCFILEPATH; // Remove directory "scripts.rc" if it is empty. It could be // mistakenly created by Lokalize 15.04.x. if (QFileInfo(filepath).isDir() && !QDir().rmdir(filepath)) { qCCritical(LOKALIZE_LOG) << "Failed to remove directory" << filepath << "to create scripting configuration file with at the same path. " << "The directory may be not empty."; return; } if (!QFile::exists(filepath)) { QDir().mkdir(QFileInfo(filepath).dir().path()); QFile f(filepath); if (!f.open(QIODevice::WriteOnly)) return; QTextStream out(&f); out << ""; f.close(); } //qCWarning(LOKALIZE_LOG)<collection(Project::instance()->kind()); if (!collection) return; foreach (const QString &collectionname, collection->collections()) { Kross::ActionCollection* c = collection->collection(collectionname); if (!c->isEnabled()) continue; foreach (Kross::Action* action, c->actions()) { if (action->property("autorun").toBool()) action->trigger(); if (action->property("first-run").toBool() && Project::local()->firstRun()) action->trigger(); } } } ProjectScriptingPlugin::~ProjectScriptingPlugin() { Kross::ActionCollection* collection = Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()); if (!collection) return; QString scriptsrc = PROJECTRCFILE; QDir rcdir(PROJECTRCFILEDIR); qCDebug(LOKALIZE_LOG) << rcdir.entryList(QStringList("*.rc"), QDir::Files); foreach (const QString& rc, QDir(PROJECTRCFILEDIR).entryList(QStringList("*.rc"), QDir::Files)) if (rc != scriptsrc) qCWarning(LOKALIZE_LOG) << rc << collection->readXmlFile(rcdir.absoluteFilePath(rc)); } /* void LokalizeMainWindow::checkForProjectAlreadyOpened() { QStringList services=QDBusConnection::sessionBus().interface()->registeredServiceNames().value(); int i=services.size(); while(--i>=0) { if (services.at(i).startsWith("org.kde.lokalize")) //QDBusReply QDBusConnectionInterface::servicePid ( const QString & serviceName ) const; QDBusConnection::callWithCallback(QDBusMessage::createMethodCall(services.at(i),"/ThisIsWhatYouWant","org.kde.Lokalize.MainWindow","currentProject"), this, SLOT(), const char * errorMethod); } } */ void LokalizeMainWindow::registerDBusAdaptor() { new MainWindowAdaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String("/ThisIsWhatYouWant"), this); //qCWarning(LOKALIZE_LOG)<registeredServiceNames().value(); #ifndef Q_OS_MAC //TODO really fix!!! guiFactory()->addClient(new MyScriptingPlugin(this, m_multiEditorAdaptor)); #endif //QMenu* projectActions=static_cast(factory()->container("project_actions",this)); /* KActionCollection* actionCollection = mWindow->actionCollection(); actionCollection->action("file_save")->setEnabled(canSave); actionCollection->action("file_save_as")->setEnabled(canSave); */ } void LokalizeMainWindow::loadProjectScripts() { if (m_projectScriptingPlugin) { guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; } //a HACK to get new .rc files shown w/o requiring a restart m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); //guiFactory()->addClient(m_projectScriptingPlugin); //guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); guiFactory()->addClient(m_projectScriptingPlugin); } int LokalizeMainWindow::lookupInTranslationMemory(DocPosition::Part part, const QString& text) { TM::TMTab* w = showTM(); if (!text.isEmpty()) w->lookup(part == DocPosition::Source ? text : QString(), part == DocPosition::Target ? text : QString()); return w->dbusId(); } int LokalizeMainWindow::lookupInTranslationMemory(const QString& source, const QString& target) { TM::TMTab* w = showTM(); w->lookup(source, target); return w->dbusId(); } int LokalizeMainWindow::showTranslationMemory() { /*activateWindow(); raise(); show();*/ return lookupInTranslationMemory(DocPosition::UndefPart, QString()); } int LokalizeMainWindow::openFileInEditorAt(const QString& path, const QString& source, const QString& ctxt) { EditorTab* w = fileOpen(path, source, ctxt, true); if (!w) return -1; return w->dbusId(); } int LokalizeMainWindow::openFileInEditor(const QString& path) { return openFileInEditorAt(path, QString(), QString()); } QObject* LokalizeMainWindow::activeEditor() { //QList editors=m_mdiArea->subWindowList(); QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (!activeSW || !qobject_cast(activeSW->widget())) return 0; return activeSW->widget(); } QObject* LokalizeMainWindow::editorForFile(const QString& path) { FileToEditor::const_iterator it = m_fileToEditor.constFind(QFileInfo(path).canonicalFilePath()); if (it == m_fileToEditor.constEnd()) return 0; QMdiSubWindow* w = it.value(); if (!w) return 0; return static_cast(w->widget()); } int LokalizeMainWindow::editorIndexForFile(const QString& path) { EditorTab* editor = static_cast(editorForFile(path)); if (!editor) return -1; return editor->dbusId(); } QString LokalizeMainWindow::currentProject() { return Project::instance()->path(); } #ifdef Q_OS_WIN #include int LokalizeMainWindow::pid() { return GetCurrentProcessId(); } #else #include int LokalizeMainWindow::pid() { return getpid(); } #endif QString LokalizeMainWindow::dbusName() { return QStringLiteral("org.kde.lokalize-%1").arg(pid()); } void LokalizeMainWindow::busyCursor(bool busy) { busy ? QApplication::setOverrideCursor(Qt::WaitCursor) : QApplication::restoreOverrideCursor(); } void LokalizeMdiArea::activateNextSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activateNextSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } void LokalizeMdiArea::activatePreviousSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activatePreviousSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } MultiEditorAdaptor::MultiEditorAdaptor(EditorTab *parent) : EditorAdaptor(parent) { setObjectName(QStringLiteral("MultiEditorAdaptor")); connect(parent, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); } void MultiEditorAdaptor::setEditorTab(EditorTab* e) { if (parent()) disconnect(parent(), QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); if (e) connect(e, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); setParent(e); setAutoRelaySignals(false); setAutoRelaySignals(true); } void MultiEditorAdaptor::handleParentDestroy(QObject* p) { Q_UNUSED(p); setParent(0); } //END DBus interface DelayedFileOpener::DelayedFileOpener(const QVector& urls, LokalizeMainWindow* lmw) : QObject() , m_urls(urls) , m_lmw(lmw) { //do the work just after project loading is finished //(i.e. all the files from previous project session are loaded) QTimer::singleShot(1, this, &DelayedFileOpener::doOpen); } void DelayedFileOpener::doOpen() { int lastIndex = m_urls.count() - 1; for (int i = 0; i <= lastIndex; i++) m_lmw->fileOpen(m_urls.at(i), 0, /*set as active*/i == lastIndex); deleteLater(); } #include "moc_lokalizesubwindowbase.cpp" #include "moc_multieditoradaptor.cpp" diff --git a/src/project/project.cpp b/src/project/project.cpp index 6d048e6..b8916c1 100644 --- a/src/project/project.cpp +++ b/src/project/project.cpp @@ -1,500 +1,500 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "project.h" #include "lokalize_debug.h" #include "projectlocal.h" #include "prefs.h" #include "jobs.h" #include "glossary.h" #include "tmmanager.h" #include "glossarywindow.h" #include "editortab.h" #include "dbfilesmodel.h" #include "qamodel.h" #include #include #include #include #include #include #include #include #include #include "projectmodel.h" #include "webquerycontroller.h" #include #include #include #include #include #include #include #include using namespace Kross; QString getMailingList() { QString lang = QLocale::system().name(); if (lang.startsWith(QLatin1String("ca"))) return QLatin1String("kde-i18n-ca@kde.org"); if (lang.startsWith(QLatin1String("de"))) return QLatin1String("kde-i18n-de@kde.org"); if (lang.startsWith(QLatin1String("hu"))) return QLatin1String("kde-l10n-hu@kde.org"); if (lang.startsWith(QLatin1String("tr"))) return QLatin1String("kde-l10n-tr@kde.org"); if (lang.startsWith(QLatin1String("it"))) return QLatin1String("kde-i18n-it@kde.org"); if (lang.startsWith(QLatin1String("lt"))) return QLatin1String("kde-i18n-lt@kde.org"); if (lang.startsWith(QLatin1String("nb"))) return QLatin1String("i18n-nb@lister.ping.uio.no"); if (lang.startsWith(QLatin1String("nl"))) return QLatin1String("kde-i18n-nl@kde.org"); if (lang.startsWith(QLatin1String("nn"))) return QLatin1String("i18n-nn@lister.ping.uio.no"); if (lang.startsWith(QLatin1String("pt_BR"))) return QLatin1String("kde-i18n-pt_BR@kde.org"); if (lang.startsWith(QLatin1String("ru"))) return QLatin1String("kde-russian@lists.kde.ru"); if (lang.startsWith(QLatin1String("se"))) return QLatin1String("i18n-sme@lister.ping.uio.no"); if (lang.startsWith(QLatin1String("sl"))) return QLatin1String("lugos-slo@lugos.si"); return QLatin1String("kde-i18n-doc@kde.org"); } Project* Project::_instance = 0; void Project::cleanupProject() { delete Project::_instance; Project::_instance = 0; } Project* Project::instance() { if (_instance == 0) { _instance = new Project(); qAddPostRoutine(Project::cleanupProject); } return _instance; } Project::Project() : ProjectBase() , m_localConfig(new ProjectLocal()) , m_model(0) , m_glossary(new GlossaryNS::Glossary(this)) , m_glossaryWindow(0) , m_tmManagerWindow(0) { setDefaults(); /* qRegisterMetaType("DocPosition"); qDBusRegisterMetaType(); */ //QTimer::singleShot(66,this,SLOT(initLater())); } /* void Project::initLater() { if (isLoaded()) return; KConfig cfg; KConfigGroup gr(&cfg,"State"); QString file=gr.readEntry("Project"); if (!file.isEmpty()) load(file); } */ Project::~Project() { delete m_localConfig; //Project::save() } void Project::load(const QString &newProjectPath, const QString& forcedTargetLangCode, const QString& forcedProjectId) { - QElapsedTimer a; a.start(); +// QElapsedTimer a; a.start(); TM::threadPool()->clear(); qCDebug(LOKALIZE_LOG) << "loading" << newProjectPath << "finishing tm jobs..."; if (!m_path.isEmpty()) { TM::CloseDBJob* closeDBJob = new TM::CloseDBJob(projectID()); closeDBJob->setAutoDelete(true); TM::threadPool()->start(closeDBJob, CLOSEDB); } TM::threadPool()->waitForDone(500);//more safety setSharedConfig(KSharedConfig::openConfig(newProjectPath, KConfig::NoGlobals)); if (!QFileInfo::exists(newProjectPath)) Project::instance()->setDefaults(); ProjectBase::load(); m_path = newProjectPath; m_desirablePath.clear(); //cache: m_projectDir = QFileInfo(m_path).absolutePath(); m_localConfig->setSharedConfig(KSharedConfig::openConfig(projectID() + QStringLiteral(".local"), KConfig::NoGlobals, QStandardPaths::DataLocation)); m_localConfig->load(); if (forcedTargetLangCode.length()) setLangCode(forcedTargetLangCode); else if (langCode().isEmpty()) setLangCode(QLocale::system().name()); if (forcedProjectId.length()) setProjectID(forcedProjectId); //KConfig config; //delete m_localConfig; m_localConfig=new KConfigGroup(&config,"Project-"+path()); populateDirModel(); //put 'em into thread? //QTimer::singleShot(0,this,SLOT(populateGlossary())); populateGlossary();//we cant postpone it because project load can be called from define new term function m_sourceFilePaths.clear(); if (newProjectPath.isEmpty()) return; if (!isTmSupported()) qCWarning(LOKALIZE_LOG) << "no sqlite module available"; //NOTE do we need to explicitly call it when project id changes? TM::DBFilesModel::instance()->openDB(projectID(), TM::Undefined, true); if (QaModel::isInstantiated()) { QaModel::instance()->saveRules(); QaModel::instance()->loadRules(qaPath()); } //qCDebug(LOKALIZE_LOG)<<"until emitting signal"<setAutoDelete(true); TM::threadPool()->start(closeDBJob, CLOSEDB); populateDirModel(); populateGlossary(); TM::threadPool()->waitForDone(500);//more safety TM::DBFilesModel::instance()->openDB(projectID(), TM::Undefined, true); } QString Project::absolutePath(const QString& possiblyRelPath) const { if (QFileInfo(possiblyRelPath).isRelative()) return QDir::cleanPath(m_projectDir + QLatin1Char('/') + possiblyRelPath); return possiblyRelPath; } void Project::populateDirModel() { if (Q_UNLIKELY(m_path.isEmpty() || !QFileInfo::exists(poDir()))) return; QUrl potUrl; if (QFileInfo::exists(potDir())) potUrl = QUrl::fromLocalFile(potDir()); model()->setUrl(QUrl::fromLocalFile(poDir()), potUrl); } void Project::populateGlossary() { m_glossary->load(glossaryPath()); } GlossaryNS::GlossaryWindow* Project::showGlossary() { return defineNewTerm(); } GlossaryNS::GlossaryWindow* Project::defineNewTerm(QString en, QString target) { if (!SettingsController::instance()->ensureProjectIsLoaded()) return 0; if (!m_glossaryWindow) m_glossaryWindow = new GlossaryNS::GlossaryWindow(SettingsController::instance()->mainWindowPtr()); m_glossaryWindow->show(); m_glossaryWindow->activateWindow(); if (!en.isEmpty() || !target.isEmpty()) m_glossaryWindow->newTermEntry(en, target); return m_glossaryWindow; } bool Project::queryCloseForAuxiliaryWindows() { if (m_glossaryWindow && m_glossaryWindow->isVisible()) return m_glossaryWindow->queryClose(); return true; } bool Project::isTmSupported() const { QStringList drivers = QSqlDatabase::drivers(); return drivers.contains(QLatin1String("QSQLITE")); } void Project::showTMManager() { if (!m_tmManagerWindow) { if (!isTmSupported()) { KMessageBox::information(nullptr, i18n("TM facility requires SQLite Qt module."), i18n("No SQLite module available")); return; } m_tmManagerWindow = new TM::TMManagerWin(SettingsController::instance()->mainWindowPtr()); } m_tmManagerWindow->show(); m_tmManagerWindow->activateWindow(); } bool Project::isFileMissing(const QString& filePath) const { if (!QFile::exists(filePath) && isLoaded()) { //check if we are opening template QString newPath = filePath; newPath.replace(poDir(), potDir()); if (!QFile::exists(newPath) && !QFile::exists(newPath += 't')) { return true; } } return false; } void Project::save() { m_localConfig->setFirstRun(false); ProjectBase::setTargetLangCode(langCode()); ProjectBase::save(); m_localConfig->save(); } ProjectModel* Project::model() { if (Q_UNLIKELY(!m_model)) m_model = new ProjectModel(this); return m_model; } void Project::setDefaults() { ProjectBase::setDefaults(); setLangCode(QLocale::system().name()); } void Project::init(const QString& path, const QString& kind, const QString& id, const QString& sourceLang, const QString& targetLang) { setDefaults(); bool stop = false; while (true) { setKind(kind); setSourceLangCode(sourceLang); setLangCode(targetLang); setProjectID(id); if (stop) break; else { load(path); stop = true; } } save(); } static void fillFilePathsRecursive(const QDir& dir, QMultiMap& sourceFilePaths) { QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) fillFilePathsRecursive(QDir(dir.filePath(subDirs.at(i))), sourceFilePaths); static QStringList filters = QStringList(QStringLiteral("*.cpp")) << QStringLiteral("*.c") << QStringLiteral("*.cc") << QStringLiteral("*.mm") << QStringLiteral("*.ui") << QStringLiteral("*rc"); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); QByteArray absDirPath = dir.absolutePath().toUtf8(); absDirPath.squeeze(); while (--i >= 0) { //qCDebug(LOKALIZE_LOG)<sourceFilePathsAreReady(); } protected: bool doKill() override; private: QString m_folderName; }; SourceFilesSearchJob::SourceFilesSearchJob(const QString& folderName, QObject* parent) : KJob(parent) , m_folderName(folderName) { setCapabilities(KJob::Killable); } bool SourceFilesSearchJob::doKill() { //TODO return true; } class FillSourceFilePathsJob: public QRunnable { public: explicit FillSourceFilePathsJob(const QDir& dir, SourceFilesSearchJob* j): startingDir(dir), kj(j) {} protected: void run() override { QMultiMap sourceFilePaths; fillFilePathsRecursive(startingDir, sourceFilePaths); Project::instance()->m_sourceFilePaths = sourceFilePaths; QTimer::singleShot(0, kj, &SourceFilesSearchJob::finish); } public: QDir startingDir; SourceFilesSearchJob* kj; }; void SourceFilesSearchJob::start() { QThreadPool::globalInstance()->start(new FillSourceFilePathsJob(QDir(m_folderName), this)); emit description(this, i18n("Scanning folders with source files"), qMakePair(i18n("Editor"), m_folderName)); } const QMultiMap& Project::sourceFilePaths() { if (m_sourceFilePaths.isEmpty()) { QDir dir(local()->sourceDir()); if (dir.exists()) { SourceFilesSearchJob* metaJob = new SourceFilesSearchJob(local()->sourceDir()); KIO::getJobTracker()->registerJob(metaJob); metaJob->start(); //KNotification* notification=new KNotification("SourceFileScan", 0); //notification->setText( i18nc("@info","Please wait while %1 is being scanned for source files.", local()->sourceDir()) ); //notification->sendEvent(); } } return m_sourceFilePaths; } #include #include #include "languagelistmodel.h" void Project::projectOdfCreate() { QString odf2xliff = QStringLiteral("odf2xliff"); if (QProcess::execute(odf2xliff, QStringList(QLatin1String("--version"))) == -2) { KMessageBox::error(SettingsController::instance()->mainWindowPtr(), i18n("Install translate-toolkit package and retry")); return; } QString odfPath = QFileDialog::getOpenFileName(SettingsController::instance()->mainWindowPtr(), QString(), QDir::homePath()/*_catalog->url().directory()*/, i18n("OpenDocument files (*.odt *.ods)")/*"text/x-lokalize-project"*/); if (odfPath.isEmpty()) return; QString targetLangCode = getTargetLangCode(QString(), true); QFileInfo fi(odfPath); QString trFolderName = i18nc("project folder name. %2 is targetLangCode", "%1 %2 Translation", fi.baseName(), targetLangCode); fi.absoluteDir().mkdir(trFolderName); QStringList args(odfPath); args.append(fi.absoluteDir().absoluteFilePath(trFolderName) + '/' + fi.baseName() + QLatin1String(".xlf")); qCDebug(LOKALIZE_LOG) << args; QProcess::execute(odf2xliff, args); if (!QFile::exists(args.at(1))) return; emit closed(); Project::instance()->load(fi.absoluteDir().absoluteFilePath(trFolderName) + QLatin1String("/index.lokalize"), targetLangCode, fi.baseName() + '-' + targetLangCode); emit fileOpenRequested(args.at(1), true); } diff --git a/src/project/projecttab.cpp b/src/project/projecttab.cpp index cc366e8..bc8205e 100644 --- a/src/project/projecttab.cpp +++ b/src/project/projecttab.cpp @@ -1,470 +1,490 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "projecttab.h" #include "project.h" #include "projectwidget.h" #include "tmscanapi.h" #include "prefs.h" #include "prefs_lokalize.h" #include "catalog.h" #include "lokalize_debug.h" -#include #include #include +#include +#include +#include +#include #include +#include #include -#include -#include #include #include #include #include #include #include #include #include #include #include #include #include ProjectTab::ProjectTab(QWidget *parent) : LokalizeSubwindowBase2(parent) , m_browser(new ProjectWidget(this)) , m_filterEdit(new QLineEdit(this)) , m_pologyProcessInProgress(false) , m_legacyUnitsCount(-1) , m_currentUnitsCount(0) { setWindowTitle(i18nc("@title:window", "Project Overview")); //setCaption(i18nc("@title:window","Project"),false); //BEGIN setup welcome widget QWidget* welcomeWidget = new QWidget(this); QVBoxLayout* wl = new QVBoxLayout(welcomeWidget); QLabel* about = new QLabel(i18n("" //copied from kaboutkdedialog_p.cpp "You do not have to be a software developer to be a member of the " "KDE team. You can join the national teams that translate " "program interfaces. You can provide graphics, themes, sounds, and " "improved documentation. You decide!" "

    " "Visit " "%1 " "for information on some projects in which you can participate." "

    " "If you need more information or documentation, then a visit to " "%2 " "will provide you with what you need.", QLatin1String("https://community.kde.org/Get_Involved"), QLatin1String("https://techbase.kde.org/")), welcomeWidget); about->setAlignment(Qt::AlignCenter); about->setWordWrap(true); about->setOpenExternalLinks(true); about->setTextInteractionFlags(Qt::TextBrowserInteraction); about->setTextFormat(Qt::RichText); QPushButton* conf = new QPushButton(i18n("&Configure Lokalize"), welcomeWidget); QPushButton* openProject = new QPushButton(i18nc("@action:inmenu", "Open project"), welcomeWidget); QPushButton* createProject = new QPushButton(i18nc("@action:inmenu", "Translate software"), welcomeWidget); QPushButton* createOdfProject = new QPushButton(i18nc("@action:inmenu", "Translate OpenDocument"), welcomeWidget); connect(conf, &QPushButton::clicked, SettingsController::instance(), &SettingsController::showSettingsDialog); connect(openProject, &QPushButton::clicked, this, QOverload<>::of(&ProjectTab::projectOpenRequested)); connect(createProject, &QPushButton::clicked, SettingsController::instance(), &SettingsController::projectCreate); connect(createOdfProject, &QPushButton::clicked, Project::instance(), &Project::projectOdfCreate); QHBoxLayout* wbtnl = new QHBoxLayout(); wbtnl->addStretch(1); wbtnl->addWidget(conf); wbtnl->addWidget(openProject); wbtnl->addWidget(createProject); wbtnl->addWidget(createOdfProject); wbtnl->addStretch(1); wl->addStretch(1); wl->addWidget(about); wl->addStretch(1); wl->addLayout(wbtnl); wl->addStretch(1); //END setup welcome widget QWidget* baseWidget = new QWidget(this); m_stackedLayout = new QStackedLayout(baseWidget); QWidget* w = new QWidget(this); m_stackedLayout->addWidget(welcomeWidget); m_stackedLayout->addWidget(w); connect(Project::instance(), &Project::loaded, this, &ProjectTab::showRealProjectOverview); if (Project::instance()->isLoaded()) //for --project cmd option showRealProjectOverview(); QVBoxLayout* l = new QVBoxLayout(w); m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setPlaceholderText(i18n("Quick search...")); m_filterEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); connect(m_filterEdit, &QLineEdit::textChanged, this, &ProjectTab::setFilterRegExp, Qt::QueuedConnection); new QShortcut(Qt::CTRL + Qt::Key_L, this, SLOT(setFocus()), 0, Qt::WidgetWithChildrenShortcut); l->addWidget(m_filterEdit); l->addWidget(m_browser); connect(m_browser, &ProjectWidget::fileOpenRequested, this, &ProjectTab::fileOpenRequested); connect(Project::instance()->model(), &ProjectModel::totalsChanged, this, &ProjectTab::updateStatusBar); connect(Project::instance()->model(), &ProjectModel::loadingAboutToStart, this, &ProjectTab::initStatusBarProgress); setCentralWidget(baseWidget); QStatusBar* statusBar = static_cast(parent)->statusBar(); m_progressBar = new QProgressBar(0); m_progressBar->setVisible(false); statusBar->insertWidget(ID_STATUS_PROGRESS, m_progressBar, 1); setXMLFile(QStringLiteral("projectmanagerui.rc"), true); setUpdatedXMLFile(); //QAction* action = KStandardAction::find(Project::instance(),&ProjectTab::showTM,actionCollection()); #define ADD_ACTION_SHORTCUT_ICON(_name,_text,_shortcut,_icon)\ action = nav->addAction(QStringLiteral(_name));\ action->setText(_text);\ action->setIcon(QIcon::fromTheme(_icon));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut )); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* nav = new KActionCategory(i18nc("@title actions category", "Navigation"), ac); ADD_ACTION_SHORTCUT_ICON("go_prev_fuzzyUntr", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Previous not ready"), Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp, "prevfuzzyuntrans") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevFuzzyUntr); ADD_ACTION_SHORTCUT_ICON("go_next_fuzzyUntr", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Next not ready"), Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown, "nextfuzzyuntrans") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextFuzzyUntr); ADD_ACTION_SHORTCUT_ICON("go_prev_fuzzy", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Previous non-empty but not ready"), Qt::CTRL + Qt::Key_PageUp, "prevfuzzy") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevFuzzy); ADD_ACTION_SHORTCUT_ICON("go_next_fuzzy", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Next non-empty but not ready"), Qt::CTRL + Qt::Key_PageDown, "nextfuzzy") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextFuzzy); ADD_ACTION_SHORTCUT_ICON("go_prev_untrans", i18nc("@action:inmenu", "Previous untranslated"), Qt::ALT + Qt::Key_PageUp, "prevuntranslated") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevUntranslated); ADD_ACTION_SHORTCUT_ICON("go_next_untrans", i18nc("@action:inmenu", "Next untranslated"), Qt::ALT + Qt::Key_PageDown, "nextuntranslated") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextUntranslated); ADD_ACTION_SHORTCUT_ICON("go_prev_templateOnly", i18nc("@action:inmenu", "Previous template only"), Qt::CTRL + Qt::Key_Up, "prevtemplate") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevTemplateOnly); ADD_ACTION_SHORTCUT_ICON("go_next_templateOnly", i18nc("@action:inmenu", "Next template only"), Qt::CTRL + Qt::Key_Down, "nexttemplate") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTemplateOnly); ADD_ACTION_SHORTCUT_ICON("go_prev_transOnly", i18nc("@action:inmenu", "Previous translation only"), Qt::ALT + Qt::Key_Up, "prevpo") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevTransOnly); ADD_ACTION_SHORTCUT_ICON("go_next_transOnly", i18nc("@action:inmenu", "Next translation only"), Qt::ALT + Qt::Key_Down, "nextpo") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTransOnly); action = nav->addAction(QStringLiteral("toggle_translated_files")); action->setText(i18nc("@action:inmenu", "Hide completed items")); action->setToolTip(i18nc("@action:inmenu", "Hide fully translated files and folders")); action->setIcon(QIcon::fromTheme("hide_table_row")); action->setCheckable(true); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(action, &QAction::triggered, this, &ProjectTab::toggleTranslatedFiles); // ADD_ACTION_SHORTCUT_ICON("edit_find",i18nc("@action:inmenu","Find in files"),Qt::ALT+Qt::Key_Down,"nextpo") //connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTransOnly); action = nav->addAction(KStandardAction::Find, this, SLOT(searchInFiles())); KActionCategory* proj = new KActionCategory(i18nc("@title actions category", "Project"), ac); action = proj->addAction(QStringLiteral("project_open"), this, SIGNAL(projectOpenRequested())); action->setText(i18nc("@action:inmenu", "Open project")); action->setIcon(QIcon::fromTheme("project-open")); int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); } void ProjectTab::showRealProjectOverview() { m_stackedLayout->setCurrentIndex(1); } void ProjectTab::showWelcomeScreen() { m_stackedLayout->setCurrentIndex(0); } void ProjectTab::toggleTranslatedFiles() { m_browser->toggleTranslatedFiles(); } QString ProjectTab::currentFilePath() { return Project::instance()->path(); } void ProjectTab::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void ProjectTab::setFilterRegExp() { QString newPattern = m_filterEdit->text(); if (m_browser->proxyModel()->filterRegExp().pattern() == newPattern) return; m_browser->proxyModel()->setFilterRegExp(newPattern); if (newPattern.size() > 2) m_browser->expandItems(); } void ProjectTab::contextMenuEvent(QContextMenuEvent *event) { QMenu* menu = new QMenu(this); connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); if (m_browser->selectedItems().size() > 1 || (m_browser->selectedItems().size() == 1 && !m_browser->currentIsTranslationFile())) { menu->addAction(i18nc("@action:inmenu", "Open selected files"), this, &ProjectTab::openFile); menu->addSeparator(); } else if (m_browser->currentIsTranslationFile()) { menu->addAction(i18nc("@action:inmenu", "Open"), this, &ProjectTab::openFile); menu->addSeparator(); } /*menu.addAction(i18nc("@action:inmenu","Find in files"),this,&ProjectTab::findInFiles); menu.addAction(i18nc("@action:inmenu","Replace in files"),this,&ProjectTab::replaceInFiles); menu.addAction(i18nc("@action:inmenu","Spellcheck files"),this,&ProjectTab::spellcheckFiles); menu.addSeparator(); menu->addAction(i18nc("@action:inmenu","Get statistics for subfolders"),m_browser,&ProjectTab::expandItems); */ menu->addAction(i18nc("@action:inmenu", "Add to translation memory"), this, &ProjectTab::scanFilesToTM); menu->addAction(i18nc("@action:inmenu", "Search in files"), this, &ProjectTab::searchInFiles); if (Settings::self()->pologyEnabled()) { menu->addAction(i18nc("@action:inmenu", "Launch Pology on files"), this, &ProjectTab::pologyOnFiles); } if (QDir(Project::instance()->templatesRoot()).exists()) menu->addAction(i18nc("@action:inmenu", "Search in files (including templates)"), this, &ProjectTab::searchInFilesInclTempl); // else if (Project::instance()->model()->hasChildren(/*m_proxyModel->mapToSource(*/(m_browser->currentIndex())) // ) // { // menu.addSeparator(); // menu.addAction(i18n("Force Scanning"),this,&ProjectTab::slotForceStats); // // } menu->popup(event->globalPos()); } void ProjectTab::scanFilesToTM() { TM::scanRecursive(m_browser->selectedItems(), Project::instance()->projectID()); } void ProjectTab::searchInFiles(bool templ) { QStringList files = m_browser->selectedItems(); if (!templ) { QString templatesRoot = Project::instance()->templatesRoot(); int i = files.size(); while (--i >= 0) { if (files.at(i).startsWith(templatesRoot)) files.removeAt(i); } } emit searchRequested(files); } void ProjectTab::pologyOnFiles() { if (!m_pologyProcessInProgress) { QStringList files = m_browser->selectedItems(); QString templatesRoot = Project::instance()->templatesRoot(); QString filesAsString; int i = files.size(); while (--i >= 0) { if (files.at(i).endsWith(QStringLiteral(".po"))) filesAsString += QStringLiteral("\"") + files.at(i) + QStringLiteral("\" "); } QString command = Settings::self()->pologyCommandFile().replace(QStringLiteral("%f"), filesAsString); m_pologyProcess = new KProcess; m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); qCWarning(LOKALIZE_LOG) << "Launching pology command: " << command; connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &ProjectTab::pologyHasFinished); m_pologyProcess->setShellCommand(command); m_pologyProcessInProgress = true; m_pologyProcess->start(); } else { KMessageBox::error(this, i18n("A Pology check is already in progress."), i18n("Pology error")); } } void ProjectTab::pologyHasFinished(int exitCode, QProcess::ExitStatus exitStatus) { const QString pologyError = m_pologyProcess->readAllStandardError(); if (exitStatus == QProcess::CrashExit) { KMessageBox::error(this, i18n("The Pology check has crashed unexpectedly:\n%1", pologyError), i18n("Pology error")); } else if (exitCode == 0) { KMessageBox::information(this, i18n("The Pology check has succeeded"), i18n("Pology success")); } else { KMessageBox::error(this, i18n("The Pology check has returned an error:\n%1", pologyError), i18n("Pology error")); } m_pologyProcess->deleteLater(); m_pologyProcessInProgress = false; } void ProjectTab::searchInFilesInclTempl() { searchInFiles(true); } void ProjectTab::openFile() { QStringList files = m_browser->selectedItems(); int i = files.size(); + + if (i > 50) { + QString caption = i18np("You are about to open %1 file", "You are about to open %1 files", i); + QString text = i18n("Opening a large number of files at the same time can make Lokalize unresponsive.") + + QStringLiteral("\n\n") + + i18n("Are you sure you want to open this many files?"); + auto yes = KGuiItem( + i18np("&Open %1 File", "&Open %1 Files", i), + QStringLiteral("document-open") + ); + const int answer = KMessageBox::warningYesNo( + this, text, caption, yes, KStandardGuiItem::cancel() + ); + if (answer != KMessageBox::Yes) { + return; + } + } + while (--i >= 0) { if (Catalog::extIsSupported(files.at(i))) { emit fileOpenRequested(files.at(i), true); } } } void ProjectTab::findInFiles() { emit searchRequested(m_browser->selectedItems()); } void ProjectTab::replaceInFiles() { emit replaceRequested(m_browser->selectedItems()); } void ProjectTab::spellcheckFiles() { emit spellcheckRequested(m_browser->selectedItems()); } void ProjectTab::gotoPrevFuzzyUntr() { m_browser->gotoPrevFuzzyUntr(); } void ProjectTab::gotoNextFuzzyUntr() { m_browser->gotoNextFuzzyUntr(); } void ProjectTab::gotoPrevFuzzy() { m_browser->gotoPrevFuzzy(); } void ProjectTab::gotoNextFuzzy() { m_browser->gotoNextFuzzy(); } void ProjectTab::gotoPrevUntranslated() { m_browser->gotoPrevUntranslated(); } void ProjectTab::gotoNextUntranslated() { m_browser->gotoNextUntranslated(); } void ProjectTab::gotoPrevTemplateOnly() { m_browser->gotoPrevTemplateOnly(); } void ProjectTab::gotoNextTemplateOnly() { m_browser->gotoNextTemplateOnly(); } void ProjectTab::gotoPrevTransOnly() { m_browser->gotoPrevTransOnly(); } void ProjectTab::gotoNextTransOnly() { m_browser->gotoNextTransOnly(); } bool ProjectTab::currentItemIsTranslationFile() const { return m_browser->currentIsTranslationFile(); } void ProjectTab::setCurrentItem(const QString& url) { m_browser->setCurrentItem(url); } QString ProjectTab::currentItem() const { return m_browser->currentItem(); } QStringList ProjectTab::selectedItems() const { return m_browser->selectedItems(); } void ProjectTab::updateStatusBar(int fuzzy, int translated, int untranslated, bool done) { int total = fuzzy + translated + untranslated; m_currentUnitsCount = total; if (m_progressBar->value() != total && m_legacyUnitsCount > 0) m_progressBar->setValue(total); if (m_progressBar->maximum() < qMax(total, m_legacyUnitsCount)) m_progressBar->setMaximum(qMax(total, m_legacyUnitsCount)); m_progressBar->setVisible(!done); if (done) m_legacyUnitsCount = total; statusBarItems.insert(ID_STATUS_TOTAL, i18nc("@info:status message entries", "Total: %1", total)); reflectNonApprovedCount(fuzzy, total); reflectUntranslatedCount(untranslated, total); } void ProjectTab::initStatusBarProgress() { if (m_legacyUnitsCount > 0) { if (m_progressBar->value() != 0) m_progressBar->setValue(0); if (m_progressBar->maximum() != m_legacyUnitsCount) m_progressBar->setMaximum(m_legacyUnitsCount); updateStatusBar(); } } void ProjectTab::setLegacyUnitsCount(int to) { m_legacyUnitsCount = to; m_currentUnitsCount = to; initStatusBarProgress(); } //bool ProjectTab::isShown() const {return isVisible();} diff --git a/src/tm/dbfilesmodel.h b/src/tm/dbfilesmodel.h index 597a4d7..d6387b4 100644 --- a/src/tm/dbfilesmodel.h +++ b/src/tm/dbfilesmodel.h @@ -1,113 +1,113 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef DBFILESMODEL_H #define DBFILESMODEL_H #include "jobs.h" #include -#include +#include #include class QFileSystemModel; class QPersistentModelIndex; namespace TM { class OpenDBJob; class DBFilesModel: public QSortFilterProxyModel { Q_OBJECT public: enum Columns { Name = 0, SourceLang, TargetLang, Pairs, OriginalsCount, TranslationsCount, ColumnCount }; enum Rolse { FileNameRole = Qt::UserRole + 50, NameRole = Qt::UserRole + 51 }; DBFilesModel(); ~DBFilesModel() override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; Qt::ItemFlags flags(const QModelIndex&) const override { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex rootIndex() const; void removeTM(QModelIndex); //can be zero!!! QPersistentModelIndex* projectDBIndex()const { return projectDB; } void openDB(const QString& name, DbType type = Undefined, bool forceCurrentProjectConfig = false); void openDB(OpenDBJob*); static DBFilesModel* instance(); private: static DBFilesModel* _instance; static void cleanupDBFilesModel(); protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; public slots: void updateStats(const QModelIndex& topLeft, const QModelIndex& bottomRight); void calcStats(const QModelIndex& parent, int start, int end); void openJobDone(OpenDBJob*); void closeJobDone(CloseDBJob*); void updateProjectTmIndex(); private: mutable QPersistentModelIndex* projectDB; QFileSystemModel* m_fileSystemModel; QString m_tmRootPath; - QTime m_timeSinceLastUpdate; + QElapsedTimer m_timeSinceLastUpdate; QMap m_stats; public: QMap m_configurations; QList m_openingDb; QMutex m_openingDbLock; }; } #endif diff --git a/src/tm/jobs.cpp b/src/tm/jobs.cpp index ca6b98a..a06f7f2 100644 --- a/src/tm/jobs.cpp +++ b/src/tm/jobs.cpp @@ -1,2135 +1,2136 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "jobs.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include "diff.h" #include "prefs_lokalize.h" #include "version.h" #include "stemming.h" #include #include #include #include #include #include #include #include +#include #include #include #include using namespace TM; QThreadPool* TM::threadPool() { static QThreadPool* inst = new QThreadPool; return inst; } #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif #define TM_DELIMITER '\v' #define TM_SEPARATOR '\b' #define TM_NOTAPPROVED 0x04 static bool stop = false; void TM::cancelAllJobs() { stop = true; } static qlonglong newTMSourceEntryCount = 0; static qlonglong reusedTMSourceEntryCount = 0; /** * splits string into words, removing any markup * * TODO segmentation by sentences... **/ static void doSplit(QString& cleanEn, QStringList& words, QRegExp& rxClean1, const QString& accel ) { static QRegExp rxSplit(QStringLiteral("\\W+|\\d+")); if (!rxClean1.pattern().isEmpty()) cleanEn.replace(rxClean1, QStringLiteral(" ")); cleanEn.remove(accel); words = cleanEn.toLower().split(rxSplit, QString::SkipEmptyParts); if (words.size() > 4) { int i = 0; for (; i < words.size(); ++i) { if (words.at(i).size() < 4) words.removeAt(i--); else if (words.at(i).startsWith('t') && words.at(i).size() == 4) { if (words.at(i) == QLatin1String("then") || words.at(i) == QLatin1String("than") || words.at(i) == QLatin1String("that") || words.at(i) == QLatin1String("this") ) words.removeAt(i--); } } } } static qlonglong getFileId(const QString& path, QSqlDatabase& db) { QSqlQuery query1(db); QString escapedPath = path; escapedPath.replace(QLatin1Char('\''), QLatin1String("''")); QString pathExpr = QStringLiteral("path='") + escapedPath + '\''; if (path.isEmpty()) pathExpr = QStringLiteral("path ISNULL"); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "path='") + escapedPath + '\''))) qCWarning(LOKALIZE_LOG) << "select db error: " << query1.lastError().text(); if (Q_LIKELY(query1.next())) { //this is translation of en string that is already present in db qlonglong id = query1.value(0).toLongLong(); query1.clear(); return id; } query1.clear(); //nope, this is new file bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QString sql = QStringLiteral("INSERT INTO files (path) VALUES (?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.prepare(sql); query1.bindValue(0, path); if (Q_LIKELY(query1.exec())) return qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); else qCWarning(LOKALIZE_LOG) << "insert db error: " << query1.lastError().text(); return -1; } static void addToIndex(qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // insert word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") + words.at(j) + '\''))) qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); //we _have_ it bool weHaveIt = query1.next(); if (weHaveIt) { //just add new id QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' + sourceIdStr + ' ') || arr.startsWith(sourceIdStr + ' ') || arr.endsWith(' ' + sourceIdStr) || arr == sourceIdStr) return;//this string is already indexed query1.prepare(QStringLiteral("UPDATE words SET ") + field + QStringLiteral("=? WHERE word='") + words.at(j) + '\''); if (!arr.isEmpty()) arr += ' '; arr += sourceIdStr; query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 4: " << query1.lastError().text(); } else { query1.clear(); query1.prepare(QStringLiteral("INSERT INTO words (word, ids_short, ids_long) VALUES (?, ?, ?)")); QByteArray idsShort; QByteArray idsLong; if (isShort) idsShort = sourceIdStr; else idsLong = sourceIdStr; query1.bindValue(0, words.at(j)); query1.bindValue(1, idsShort); query1.bindValue(2, idsLong); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "insert error 2: " << query1.lastError().text() ; } } } /** * remove source string from index if there are no other * 'good' entries using it but the entry specified with mainId */ static void removeFromIndex(qlonglong mainId, qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); //BEGIN check //TM_NOTAPPROVED=4 if (Q_UNLIKELY(!query1.exec(U("SELECT count(*) FROM main, target_strings WHERE " "main.source=") + QString::number(sourceId) + U(" AND " "main.target=target_strings.id AND " "target_strings.target NOTNULL AND " "main.id!=") + QString::number(mainId) + U(" AND " "(main.bits&4)!=4")))) { qCWarning(LOKALIZE_LOG) << "select error 500: " << query1.lastError().text(); return; } bool exit = query1.next() && (query1.value(0).toLongLong() > 0); query1.clear(); if (exit) return; //END check bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // remove from record for the word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") + words.at(j) + '\''))) { qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); return; } if (!query1.next()) { qCWarning(LOKALIZE_LOG) << "exit here 1"; //we don't have record for the word, so nothing to remove query1.clear(); return; } QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' + sourceIdStr + ' ')) arr.replace(' ' + sourceIdStr + ' ', " "); else if (arr.startsWith(sourceIdStr + ' ')) arr.remove(0, sourceIdStr.size() + 1); else if (arr.endsWith(' ' + sourceIdStr)) arr.chop(sourceIdStr.size() + 1); else if (arr == sourceIdStr) arr.clear(); query1.prepare(U("UPDATE words " "SET ") + field + U("=? " "WHERE word='") + words.at(j) + '\''); query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 504: " << query1.lastError().text(); } } static bool doRemoveEntry(qlonglong mainId, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT source_strings.id, source_strings.source FROM source_strings, main WHERE " "source_strings.id=main.source AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong sourceId = query1.value(0).toLongLong(); const QString sourceString = query1.value(1).toString(); query1.clear(); if (Q_UNLIKELY(!query1.exec(U("SELECT target_strings.id FROM target_strings, main WHERE target_strings.id=main.target AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong targetId = query1.value(0).toLongLong(); query1.clear(); query1.exec(QStringLiteral("DELETE FROM main WHERE source=") + QString::number(sourceId) + QStringLiteral(" AND target=") + QString::number(targetId)); if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE source=") + QString::number(sourceId)) || !query1.next()) return false; const bool noSourceLeft = query1.value(0).toInt() == 0; query1.clear(); if (noSourceLeft) { removeFromIndex(mainId, sourceId, sourceString, rxClean1, accel, db); query1.exec(QStringLiteral("DELETE FROM source_strings WHERE id=") + QString::number(sourceId)); } if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE target=") + QString::number(targetId)) || ! query1.next()) return false; const bool noTargetLeft = query1.value(0).toInt() == 0; query1.clear(); if (noTargetLeft) query1.exec(QStringLiteral("DELETE FROM target_strings WHERE id=") + QString::number(targetId)); return true; } static bool doRemoveFile(const QString& filePath, QSqlDatabase& db) { qlonglong fileId = getFileId(filePath, db); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "id=") + QString::number(fileId)))) return false; if (!query1.next()) return false; query1.clear(); query1.exec(QStringLiteral("DELETE source_strings FROM source_strings, main WHERE source_strings.id = main.source AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE target_strings FROM target_strings, main WHERE target_strings.id = main.target AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE FROM main WHERE file = ") + QString::number(fileId)); return query1.exec(QStringLiteral("DELETE FROM files WHERE id=") + QString::number(fileId)); } static int doRemoveMissingFiles(QSqlDatabase& db, const QString& dbName, QObject *job) { int deletedFiles = 0; QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT files.path FROM files")))) return false; if (!query1.next()) return false; do { QString filePath = query1.value(0).toString(); if (Project::instance()->isFileMissing(filePath)) { qCWarning(LOKALIZE_LOG) << "Removing file " << filePath << " from translation memory"; RemoveFileJob* job_removefile = new RemoveFileJob(filePath, dbName, job); TM::threadPool()->start(job_removefile, REMOVEFILE); deletedFiles++; } } while (query1.next()); return deletedFiles; } static QString escape(QString str) { return str.replace(QLatin1Char('\''), QStringLiteral("''")); } static bool doInsertEntry(CatalogString source, CatalogString target, const QString& ctxt, //TODO QStringList -- after XLIFF bool approved, qlonglong fileId, QSqlDatabase& db, QRegExp& rxClean1,//cleaning regexps for word index update const QString& accel, qlonglong priorId, qlonglong& mainId ) { - QTime a; a.start(); +// QTime a; a.start(); mainId = -1; if (Q_UNLIKELY(source.isEmpty())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: source empty"; return false; } bool qpsql = (db.driverName() == QLatin1String("QPSQL")); //we store non-entranslaed entries to make search over all source parts possible bool untranslated = target.isEmpty(); bool shouldBeInIndex = !untranslated && approved; //remove first occurrence of accel character so that search returns words containing accel mark int sourceAccelPos = source.string.indexOf(accel); if (sourceAccelPos != -1) source.string.remove(sourceAccelPos, accel.size()); int targetAccelPos = target.string.indexOf(accel); if (targetAccelPos != -1) target.string.remove(targetAccelPos, accel.size()); //check if we already have record with the same en string QSqlQuery query1(db); QString escapedCtxt = escape(ctxt); QByteArray sourceTags = source.tagsAsByteArray(); QByteArray targetTags = target.tagsAsByteArray(); //BEGIN get sourceId query1.prepare(QString(U("SELECT id FROM source_strings WHERE " "source=? AND (source_accel%1) AND source_markup%2")).arg (sourceAccelPos != -1 ? QStringLiteral("=?") : QStringLiteral("=-1 OR source_accel ISNULL"), sourceTags.isEmpty() ? QStringLiteral(" ISNULL") : QStringLiteral("=?"))); int paranum = 0; query1.bindValue(paranum++, source.string); if (sourceAccelPos != -1) query1.bindValue(paranum++, sourceAccelPos); if (!sourceTags.isEmpty()) query1.bindValue(paranum++, sourceTags); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } qlonglong sourceId; if (!query1.next()) { //BEGIN insert source anew //qCDebug(LOKALIZE_LOG) <<"insert source anew";; ++newTMSourceEntryCount; QString sql = QStringLiteral("INSERT INTO source_strings (source, source_markup, source_accel) VALUES (?, ?, ?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.clear(); query1.prepare(sql); query1.bindValue(0, source.string); query1.bindValue(1, sourceTags); query1.bindValue(2, sourceAccelPos != -1 ? QVariant(sourceAccelPos) : QVariant()); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } sourceId = qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); query1.clear(); //update index if (shouldBeInIndex) addToIndex(sourceId, source.string, rxClean1, accel, db); //END insert source anew } else { sourceId = query1.value(0).toLongLong(); ++reusedTMSourceEntryCount; //qCDebug(LOKALIZE_LOG)<<"SOURCE ALREADY PRESENT"< there will be new record insertion and main table update below } //qCDebug(LOKALIZE_LOG)< tmConfigCache; static void setConfig(QSqlDatabase& db, const TMConfig& c) { QSqlQuery query(db); query.prepare(QStringLiteral("INSERT INTO tm_config (key, value) VALUES (?, ?)")); query.addBindValue(0); query.addBindValue(c.markup); //qCDebug(LOKALIZE_LOG)<<"setting tm db config:"<setPriority(QThread::IdlePriority); if (m_type == TM::Local) { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); QString dbFolder = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QFileInfo fileInfo(dbFolder); if (!fileInfo.exists(dbFolder)) fileInfo.absoluteDir().mkpath(fileInfo.fileName()); db.setDatabaseName(dbFolder + QLatin1Char('/') + m_dbName + TM_DATABASE_EXTENSION); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { qCDebug(LOKALIZE_LOG) << "failed to open db" << db.databaseName() << db.lastError().text(); QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } if (!initSqliteDb(db)) { //need to recreate db ;( QString filename = db.databaseName(); db.close(); QSqlDatabase::removeDatabase(m_dbName); qCWarning(LOKALIZE_LOG) << "We need to recreate the database " << filename; QFile::remove(filename); db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); db.setDatabaseName(filename); m_connectionSuccessful = db.open() && initSqliteDb(db); if (!m_connectionSuccessful) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } } } else { if (QSqlDatabase::contains(m_dbName)) { //reconnect is true QSqlDatabase::database(m_dbName).close(); QSqlDatabase::removeDatabase(m_dbName); } if (!m_connParams.isFilled()) { QFile rdb(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + m_dbName + REMOTETM_DATABASE_EXTENSION); if (!rdb.open(QIODevice::ReadOnly | QIODevice::Text)) { emit done(this); return; } QTextStream rdbParams(&rdb); m_connParams.driver = rdbParams.readLine(); m_connParams.host = rdbParams.readLine(); m_connParams.db = rdbParams.readLine(); m_connParams.user = rdbParams.readLine(); m_connParams.passwd = rdbParams.readLine(); } QSqlDatabase db = QSqlDatabase::addDatabase(m_connParams.driver, m_dbName); db.setHostName(m_connParams.host); db.setDatabaseName(m_connParams.db); db.setUserName(m_connParams.user); db.setPassword(m_connParams.passwd); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } m_connParams.user = db.userName(); initPgDb(db); } } QSqlDatabase db = QSqlDatabase::database(m_dbName); //if (!m_markup.isEmpty()||!m_accel.isEmpty()) if (m_setParams) setConfig(db, m_tmConfig); else m_tmConfig = getConfig(db); qCDebug(LOKALIZE_LOG) << "db" << m_dbName << "opened" << a.elapsed() << m_tmConfig.targetLangCode; getStats(db, m_stat.pairsCount, m_stat.uniqueSourcesCount, m_stat.uniqueTranslationsCount); if (m_type == TM::Local) { db.close(); db.open(); } emit done(this); } CloseDBJob::CloseDBJob(const QString& name) : QObject(), QRunnable() , m_dbName(name) { setAutoDelete(false); } CloseDBJob::~CloseDBJob() { qCDebug(LOKALIZE_LOG) << "closedb dtor" << m_dbName; } void CloseDBJob::run() { if (m_dbName.length()) QSqlDatabase::removeDatabase(m_dbName); emit done(this); } static QString makeAcceledString(QString source, const QString& accel, const QVariant& accelPos) { if (accelPos.isNull()) return source; int accelPosInt = accelPos.toInt(); if (accelPosInt != -1) source.insert(accelPosInt, accel); return source; } SelectJob* TM::initSelectJob(Catalog* catalog, DocPosition pos, QString db, int opt) { SelectJob* job = new SelectJob(catalog->sourceWithTags(pos), catalog->context(pos.entry).first(), catalog->url(), pos, db.isEmpty() ? Project::instance()->projectID() : db); if (opt & Enqueue) { //deletion should be done by receiver, e.g. slotSuggestionsCame() threadPool()->start(job, SELECT); } return job; } SelectJob::SelectJob(const CatalogString& source, const QString& ctxt, const QString& file, const DocPosition& pos, const QString& dbName) : QObject(), QRunnable() , m_source(source) , m_ctxt(ctxt) , m_file(file) , m_dequeued(false) , m_pos(pos) , m_dbName(dbName) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"selectjob"< invertMap(const QMap& source) { //uses the fact that map has its keys always sorted QMap sortingMap; for (QMap::const_iterator i = source.constBegin(); i != source.constEnd(); ++i) { sortingMap.insertMulti(i.value(), i.key()); } return sortingMap; } //returns true if seen translation with >85% bool SelectJob::doSelect(QSqlDatabase& db, QStringList& words, //QList& entries, bool isShort) { bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QMap occurencies; QVector idsForWord; QSqlQuery queryWords(db); //TODO ??? not sure. make another loop before to create QList< QList > then reorder it by size static const QString queryC[] = {U("SELECT ids_long FROM words WHERE word='%1'"), U("SELECT ids_short FROM words WHERE word='%1'") }; QString queryString = queryC[isShort]; //for each word... int o = words.size(); while (--o >= 0) { //if this is not the first word occurrence, just readd ids for it if (!(!idsForWord.isEmpty() && words.at(o) == words.at(o + 1))) { idsForWord.clear(); queryWords.exec(queryString.arg(words.at(o))); if (Q_UNLIKELY(!queryWords.exec(queryString.arg(words.at(o))))) qCWarning(LOKALIZE_LOG) << "select error: " << queryWords.lastError().text() << endl; if (queryWords.next()) { QByteArray arr(queryWords.value(0).toByteArray()); queryWords.clear(); QList ids(arr.split(' ')); int p = ids.size(); idsForWord.reserve(p); while (--p >= 0) idsForWord.append(ids.at(p).toLongLong(/*bool ok*/0, 36)); } else { queryWords.clear(); continue; } } //qCWarning(LOKALIZE_LOG) <<"SelectJob: idsForWord.size() "<::const_iterator i = idsForWord.constBegin(); i != idsForWord.constEnd(); i++) occurencies[*i]++; //0 is default value } //accels are removed TMConfig c = getConfig(db); QString tmp = c.markup; if (!c.markup.isEmpty()) tmp += '|'; QRegExp rxSplit(QLatin1Char('(') + tmp + QStringLiteral("\\W+|\\d+)+")); QString sourceClean(m_source.string); sourceClean.remove(c.accel); //split m_english for use in wordDiff later--all words are needed so we cant use list we already have QStringList englishList(sourceClean.toLower().split(rxSplit, QString::SkipEmptyParts)); static QRegExp delPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); static QRegExp addPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); delPart.setMinimal(true); addPart.setMinimal(true); //QList concordanceLevels=sortedUniqueValues(occurencies); //we start from entries with higher word-concordance level QMap concordanceLevelToIds = invertMap(occurencies); if (concordanceLevelToIds.isEmpty()) return false; bool seen85 = false; int limit = 200; auto clit = concordanceLevelToIds.constEnd(); if (concordanceLevelToIds.size()) --clit; if (concordanceLevelToIds.size()) while (--limit >= 0) { if (Q_UNLIKELY(m_dequeued)) break; //for every concordance level qlonglong level = clit.key(); QString joined; while (level == clit.key()) { joined += QString::number(clit.value()) + ','; if (clit == concordanceLevelToIds.constBegin() || --limit < 0) break; --clit; } joined.chop(1); //get records containing current word QSqlQuery queryFetch(U( "SELECT id, source, source_accel, source_markup FROM source_strings WHERE " "source_strings.id IN (") + joined + ')', db); TMEntry e; while (queryFetch.next()) { e.id = queryFetch.value(0).toLongLong(); if (queryFetch.value(3).toByteArray().size()) qCDebug(LOKALIZE_LOG) << "BA" << queryFetch.value(3).toByteArray(); e.source = CatalogString(makeAcceledString(queryFetch.value(1).toString(), c.accel, queryFetch.value(2)), queryFetch.value(3).toByteArray()); if (e.source.string.contains(TAGRANGE_IMAGE_SYMBOL)) { if (!e.source.tags.size()) qCWarning(LOKALIZE_LOG) << "problem:" << queryFetch.value(3).toByteArray().size() << queryFetch.value(3).toByteArray(); } //e.target=queryFetch.value(2).toString(); //QStringList e_ctxt=queryFetch.value(3).toString().split('\b',QString::SkipEmptyParts); //e.date=queryFetch.value(4).toString(); e.markupExpr = c.markup; e.accelExpr = c.accel; e.dbName = db.connectionName(); //BEGIN calc score QString str = e.source.string; str.remove(c.accel); QStringList englishSuggList(str.toLower().split(rxSplit, QString::SkipEmptyParts)); if (englishSuggList.size() > 10 * englishList.size()) continue; //sugg is 'old' --translator has to adapt its translation to 'new'--current QString result = wordDiff(englishSuggList, englishList); //qCWarning(LOKALIZE_LOG) <<"SelectJob: doin "< 1 so we have decreased it, and increased result: / exp(0.014 * float(addLen) * log10(3.0f + addSubStrCount)); if (delLen) { //qCWarning(LOKALIZE_LOG) <<"SelectJob: delLen:"< 8500; if (seen85 && e.score < 6000) continue; if (e.score < Settings::suggScore() * 100) continue; //BEGIN fetch rest of the data QString change_author_str; QString authors_table_str; if (qpsql) { //change_author_str=", main.change_author "; change_author_str = QStringLiteral(", pg_user.usename "); authors_table_str = QStringLiteral(" JOIN pg_user ON (pg_user.usesysid=main.change_author) "); } QSqlQuery queryRest(U( "SELECT main.id, main.date, main.ctxt, main.bits, " "target_strings.target, target_strings.target_accel, target_strings.target_markup, " "files.path, main.change_date ") + change_author_str + U( "FROM main JOIN target_strings ON (target_strings.id=main.target) JOIN files ON (files.id=main.file) ") + authors_table_str + U("WHERE " "main.source=") + QString::number(e.id) + U(" AND " "(main.bits&4)!=4 AND " "target_strings.target NOTNULL") , db); //ORDER BY tm_main.id ? queryRest.exec(); //qCDebug(LOKALIZE_LOG)<<"main select error"< sortedEntryList; //to eliminate same targets from different files while (queryRest.next()) { e.id = queryRest.value(0).toLongLong(); e.date = queryRest.value(1).toDate(); e.ctxt = queryRest.value(2).toString(); e.target = CatalogString(makeAcceledString(queryRest.value(4).toString(), c.accel, queryRest.value(5)), queryRest.value(6).toByteArray()); QStringList matchData = queryRest.value(2).toString().split(TM_DELIMITER, QString::KeepEmptyParts); //context|plural e.file = queryRest.value(7).toString(); if (e.target.isEmpty()) continue; e.obsolete = queryRest.value(3).toInt() & 1; e.changeDate = queryRest.value(8).toDate(); if (qpsql) e.changeAuthor = queryRest.value(9).toString(); //BEGIN exact match score++ if (possibleExactMatch) { //"exact" match (case insensitive+w/o non-word characters!) if (m_source.string == e.source.string) e.score = 10000; else e.score = 9900; } if (!m_ctxt.isEmpty() && matchData.size() > 0) { //check not needed? if (matchData.at(0) == m_ctxt) e.score += 33; } //qCWarning(LOKALIZE_LOG)<<"m_pos"< 1) { int form = matchData.at(1).toInt(); //pluralMatches=(form&&form==m_pos.form); if (form && form == (int)m_pos.form) { //qCWarning(LOKALIZE_LOG)<<"this"< hash; int oldCount = m_entries.size(); QMap::const_iterator it = sortedEntryList.constEnd(); if (sortedEntryList.size()) while (true) { --it; const TMEntry& e = it.key(); int& hits = hash[e.target.string]; if (!hits) //0 was default value m_entries.append(e); hits++; if (it == sortedEntryList.constBegin()) break; } for (int i = oldCount; i < m_entries.size(); ++i) m_entries[i].hits = hash.value(m_entries.at(i).target.string); //END fetch rest of the data } queryFetch.clear(); if (clit == concordanceLevelToIds.constBegin()) break; if (seen85) limit = qMin(limit, 100); //be more restrictive for the next concordance levels } return seen85; } void SelectJob::run() { //qCDebug(LOKALIZE_LOG)<<"select started"<setPriority(QThread::IdlePriority); - QTime a; a.start(); +// QTime a; a.start(); if (Q_UNLIKELY(!QSqlDatabase::contains(m_dbName))) { emit done(this); return; } QSqlDatabase db = QSqlDatabase::database(m_dbName); if (Q_UNLIKELY(!db.isValid() || !db.isOpen())) { emit done(this); return; } //qCDebug(LOKALIZE_LOG)<<"select started 2"<()); const int limit = qMin(Settings::suggCount(), m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = m_entries.size() - 1; while (i >= 0 && (i >= limit || m_entries.last().score < minScore)) { m_entries.removeLast(); i--; } if (Q_UNLIKELY(m_dequeued)) { emit done(this); return; } ++i; while (--i >= 0) { m_entries[i].accelExpr = c.accel; m_entries[i].markupExpr = c.markup; m_entries[i].diff = userVisibleWordDiff(m_entries.at(i).source.string, m_source.string, m_entries.at(i).accelExpr, m_entries.at(i).markupExpr); } emit done(this); } ScanJob::ScanJob(const QString& filePath, const QString& dbName) : QRunnable() , m_filePath(filePath) , m_time(0) , m_added(0) , m_newVersions(0) , m_size(0) , m_dbName(dbName) { qCDebug(LOKALIZE_LOG) << m_dbName << m_filePath; } void ScanJob::run() { if (stop || !QSqlDatabase::contains(m_dbName)) { return; } qCDebug(LOKALIZE_LOG) << "scan job started for" << m_filePath << m_dbName << stop << m_dbName; //QThread::currentThread()->setPriority(QThread::IdlePriority); - QTime a; a.start(); + QElapsedTimer a; a.start(); QSqlDatabase db = QSqlDatabase::database(m_dbName); if (!db.isOpen()) return; //initSqliteDb(db); TMConfig c = getConfig(db, true); QRegExp rxClean1(c.markup); rxClean1.setMinimal(true); Catalog catalog(0); if (Q_LIKELY(catalog.loadFromUrl(m_filePath, QString(), &m_size, /*no auto save*/true) == 0)) { if (c.targetLangCode != catalog.targetLangCode()) { qCWarning(LOKALIZE_LOG) << "not indexing file because target languages don't match:" << c.targetLangCode << "in TM vs" << catalog.targetLangCode() << "in file"; return; } qlonglong priorId = -1; QSqlQuery queryBegin(QStringLiteral("BEGIN"), db); //qCWarning(LOKALIZE_LOG) <<"queryBegin error: " < #include /** @author Nick Shaforostoff */ class TmxParser : public QXmlDefaultHandler { enum State { //localstate for getting chars into right place null = 0, seg, propContext, propFile, propPluralForm, propApproved }; enum Lang { Source, Target, Null }; public: TmxParser(const QString& dbName); ~TmxParser(); private: bool startDocument() override; bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&) override; bool endElement(const QString&, const QString&, const QString&) override; bool characters(const QString&) override; private: QSqlDatabase db; QRegExp rxClean1; QString accel; int m_hits; CatalogString m_segment[3]; //Lang enum QList m_inlineTags; QString m_context; QString m_pluralForm; QString m_filePath; QString m_approvedString; State m_state: 8; Lang m_lang: 8; ushort m_added; QMap m_fileIds; QString m_dbLangCode; }; TmxParser::TmxParser(const QString& dbName) : m_hits(0) , m_state(null) , m_lang(Null) , m_added(0) , m_dbLangCode(Project::instance()->langCode().toLower()) { db = QSqlDatabase::database(dbName); TMConfig c = getConfig(db); rxClean1.setPattern(c.markup); rxClean1.setMinimal(true); accel = c.accel; } bool TmxParser::startDocument() { //initSqliteDb(db); m_fileIds.clear(); QSqlQuery queryBegin(QLatin1String("BEGIN"), db); m_state = null; m_lang = Null; return true; } TmxParser::~TmxParser() { QSqlQuery queryEnd(QLatin1String("END"), db); } bool TmxParser::startElement(const QString&, const QString&, const QString& qName, const QXmlAttributes& attr) { if (qName == QLatin1String("tu")) { bool ok; m_hits = attr.value(QLatin1String("usagecount")).toInt(&ok); if (!ok) m_hits = -1; m_segment[Source].clear(); m_segment[Target].clear(); m_context.clear(); m_pluralForm.clear(); m_filePath.clear(); m_approvedString.clear(); } else if (qName == QLatin1String("tuv")) { QString attrLang = attr.value(QStringLiteral("xml:lang")).toLower(); if (attrLang == QLatin1String("en")) //TODO startsWith? m_lang = Source; else if (attrLang == m_dbLangCode) m_lang = Target; else { qCWarning(LOKALIZE_LOG) << "skipping lang" << attr.value("xml:lang"); m_lang = Null; } } else if (qName == QLatin1String("prop")) { QString attrType = attr.value(QStringLiteral("type")).toLower(); if (attrType == QLatin1String("x-context")) m_state = propContext; else if (attrType == QLatin1String("x-file")) m_state = propFile; else if (attrType == QLatin1String("x-pluralform")) m_state = propPluralForm; else if (attrType == QLatin1String("x-approved")) m_state = propApproved; else m_state = null; } else if (qName == QLatin1String("seg")) { m_state = seg; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); int pos = m_segment[m_lang].string.size(); m_inlineTags.append(InlineTag(pos, pos, t, attr.value(QStringLiteral("id")))); } } return true; } bool TmxParser::endElement(const QString&, const QString&, const QString& qName) { if (qName == QLatin1String("tu")) { if (m_filePath.isEmpty()) m_filePath = QLatin1String("tmx-import"); if (!m_fileIds.contains(m_filePath)) m_fileIds.insert(m_filePath, getFileId(m_filePath, db)); qlonglong fileId = m_fileIds.value(m_filePath); if (!m_pluralForm.isEmpty()) m_context += TM_DELIMITER + m_pluralForm; qlonglong priorId = -1; bool ok = doInsertEntry(m_segment[Source], m_segment[Target], m_context, m_approvedString != QLatin1String("no"), fileId, db, rxClean1, accel, priorId, priorId); if (Q_LIKELY(ok)) ++m_added; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { InlineTag tag = m_inlineTags.takeLast(); qCWarning(LOKALIZE_LOG) << qName << tag.getElementName(); if (tag.isPaired()) { tag.end = m_segment[m_lang].string.size(); m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); } m_segment[m_lang].tags.append(tag); } } m_state = null; return true; } bool TmxParser::characters(const QString& ch) { if (m_state == seg && m_lang != Null) m_segment[m_lang].string += ch; else if (m_state == propFile) m_filePath += ch; else if (m_state == propContext) m_context += ch; else if (m_state == propPluralForm) m_pluralForm += ch; else if (m_state == propApproved) m_approvedString += ch; return true; } ImportTmxJob::ImportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ImportTmxJob::~ImportTmxJob() { qCDebug(LOKALIZE_LOG) << "ImportTmxJob dtor"; } void ImportTmxJob::run() { - QTime a; a.start(); + QElapsedTimer a; a.start(); QFile file(m_filename); if (!file.open(QFile::ReadOnly | QFile::Text)) return; TmxParser parser(m_dbName); QXmlSimpleReader reader; reader.setContentHandler(&parser); QXmlInputSource xmlInputSource(&file); if (!reader.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load" << m_filename; //qCWarning(LOKALIZE_LOG) <<"Done scanning "< ExportTmxJob::ExportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ExportTmxJob::~ExportTmxJob() { qCDebug(LOKALIZE_LOG) << "ExportTmxJob dtor"; } void ExportTmxJob::run() { - QTime a; a.start(); + QElapsedTimer a; a.start(); QFile out(m_filename); if (!out.open(QFile::WriteOnly | QFile::Text)) return; QXmlStreamWriter xmlOut(&out); xmlOut.setAutoFormatting(true); xmlOut.writeStartDocument(QStringLiteral("1.0")); xmlOut.writeStartElement(QStringLiteral("tmx")); xmlOut.writeAttribute(QStringLiteral("version"), QStringLiteral("2.0")); xmlOut.writeStartElement(QStringLiteral("header")); xmlOut.writeAttribute(QStringLiteral("creationtool"), QStringLiteral("lokalize")); xmlOut.writeAttribute(QStringLiteral("creationtoolversion"), QStringLiteral(LOKALIZE_VERSION)); xmlOut.writeAttribute(QStringLiteral("segtype"), QStringLiteral("paragraph")); xmlOut.writeAttribute(QStringLiteral("o-encoding"), QStringLiteral("UTF-8")); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("body")); QString dbLangCode = Project::instance()->langCode(); QSqlDatabase db = QSqlDatabase::database(m_dbName); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U( "SELECT main.id, main.ctxt, main.date, main.bits, " "source_strings.source, source_strings.source_accel, " "target_strings.target, target_strings.target_accel, " "files.path, main.change_date " "FROM main, source_strings, target_strings, files " "WHERE source_strings.id=main.source AND " "target_strings.id=main.target AND " "files.id=main.file")))) qCWarning(LOKALIZE_LOG) << "select error: " << query1.lastError().text(); TMConfig c = getConfig(db); const QString DATE_FORMAT = QStringLiteral("yyyyMMdd"); const QString PROP = QStringLiteral("prop"); const QString TYPE = QStringLiteral("type"); while (query1.next()) { QString source = makeAcceledString(query1.value(4).toString(), c.accel, query1.value(5)); QString target = makeAcceledString(query1.value(6).toString(), c.accel, query1.value(7)); xmlOut.writeStartElement(QStringLiteral("tu")); xmlOut.writeAttribute(QStringLiteral("tuid"), QString::number(query1.value(0).toLongLong())); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), QStringLiteral("en")); xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(source); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), dbLangCode); xmlOut.writeAttribute(QStringLiteral("creationdate"), QDate::fromString(query1.value(2).toString(), Qt::ISODate).toString(DATE_FORMAT)); xmlOut.writeAttribute(QStringLiteral("changedate"), QDate::fromString(query1.value(9).toString(), Qt::ISODate).toString(DATE_FORMAT)); QString ctxt = query1.value(1).toString(); if (!ctxt.isEmpty()) { int pos = ctxt.indexOf(TM_DELIMITER); if (pos != -1) { QString plural = ctxt; plural.remove(0, pos + 1); ctxt.remove(pos, plural.size()); xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-pluralform"); xmlOut.writeCharacters(plural); xmlOut.writeEndElement(); } if (!ctxt.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-context"); xmlOut.writeCharacters(ctxt); xmlOut.writeEndElement(); } } QString filePath = query1.value(8).toString(); if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-file"); xmlOut.writeCharacters(filePath); xmlOut.writeEndElement(); } qlonglong bits = query1.value(8).toLongLong(); if (bits & TM_NOTAPPROVED) if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-approved"); xmlOut.writeCharacters("no"); xmlOut.writeEndElement(); } xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(target); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeEndElement(); } query1.clear(); xmlOut.writeEndDocument(); out.close(); qCWarning(LOKALIZE_LOG) << "ExportTmxJob done exporting:" << a.elapsed(); m_time = a.elapsed(); } //END TMX ExecQueryJob::ExecQueryJob(const QString& queryString, const QString& dbName, QMutex *dbOperation) : QObject(), QRunnable() , query(0) , m_dbName(dbName) , m_query(queryString) , m_dbOperationMutex(dbOperation) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"ExecQueryJob"<lock(); delete query; m_dbOperationMutex->unlock(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob dtor"; } void ExecQueryJob::run() { m_dbOperationMutex->lock(); QSqlDatabase db = QSqlDatabase::database(m_dbName); qCDebug(LOKALIZE_LOG) << "ExecQueryJob" << m_dbName << "db.isOpen() =" << db.isOpen(); //temporarily: if (!db.isOpen()) qCWarning(LOKALIZE_LOG) << "ExecQueryJob db.open()=" << db.open(); query = new QSqlQuery(m_query, db); query->exec(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob done" << query->lastError().text(); m_dbOperationMutex->unlock(); emit done(this); } diff --git a/src/tm/qamodel.cpp b/src/tm/qamodel.cpp index 89d4eb8..4d9b2e9 100644 --- a/src/tm/qamodel.cpp +++ b/src/tm/qamodel.cpp @@ -1,235 +1,235 @@ /* This file is part of Lokalize Copyright (C) 2011-2012 Nick Shaforostoff 2018-2019 by Simon Depiets 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 "qamodel.h" #include "domroutines.h" #include #include #include #include #include -static QString ruleTagNames[] = {QString("source"), QString("falseFriend"), QString("target")}; +static const QString ruleTagNames[] = {QString("source"), QString("falseFriend"), QString("target")}; static QStringList domListToStringList(const QDomNodeList& nodes) { QStringList result; result.reserve(nodes.size()); for (int i = 0; i < nodes.size(); i++) result.append(nodes.at(i).toElement().text()); return result; } static QRegExp domNodeToRegExp(const QDomNode& node) { QRegExp re(node.toElement().text()); re.setMinimal(true); return re; } static QVector domListToRegExpVector(const QDomNodeList& nodes) { QVector result; result.reserve(nodes.size()); for (int i = 0; i < nodes.size(); i++) result.append(domNodeToRegExp(nodes.at(i))); return result; } QaModel* QaModel::_instance = 0; void QaModel::cleanupQaModel() { delete QaModel::_instance; QaModel::_instance = 0; } bool QaModel::isInstantiated() { return _instance != 0; } QaModel* QaModel::instance() { if (Q_UNLIKELY(_instance == 0)) { _instance = new QaModel; qAddPostRoutine(QaModel::cleanupQaModel); } return _instance; } QaModel::QaModel(QObject* parent): QAbstractListModel(parent) { } QaModel::~QaModel() { saveRules(); } int QaModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_entries.count(); } QVariant QaModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { //case ID: return i18nc("@title:column","ID"); case Source: return i18nc("@title:column Original text", "Source");; case FalseFriend: return i18nc("@title:column Translator's false friend", "False Friend"); } return QVariant(); } QVariant QaModel::data(const QModelIndex& item, int role) const { if (role == Qt::ToolTipRole) return m_filename; if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); static const QString nl("\n"); const QDomElement& entry = m_entries.at(item.row()).toElement(); return domListToStringList(entry.elementsByTagName(ruleTagNames[item.column()])).join(nl); } QVector QaModel::toVector() const { QVector rules; QDomNodeList m_categories = m_doc.elementsByTagName("category"); for (int i = 0; i < m_categories.size(); i++) { static const QString ruleTagName("rule"); QDomNodeList m_rules = m_categories.at(i).toElement().elementsByTagName(ruleTagName); for (int j = 0; j < m_rules.size(); j++) { Rule rule; rule.sources = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName(ruleTagNames[Source])); rule.falseFriends = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName(ruleTagNames[FalseFriend])); rule.targets = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName("target")); rules.append(rule); } } return rules; } bool QaModel::loadRules(const QString& filename) { QFile file(filename); if (file.open(QIODevice::ReadOnly)) { bool ok = m_doc.setContent(&file); file.close(); if (!ok) return false; } else { m_doc.setContent(QByteArray( "\n" "\n" " \n" " \n" "\n")); } m_entries = m_doc.elementsByTagName("rule"); m_filename = filename; return true; } bool QaModel::saveRules(QString filename) { if (filename.isEmpty()) filename = m_filename; if (filename.isEmpty()) return false; QFile device(filename); if (!device.open(QFile::WriteOnly | QFile::Truncate)) return false; QTextStream stream(&device); m_doc.save(stream, 2); //setClean(true); return true; } QModelIndex QaModel::appendRow() { beginInsertRows(QModelIndex(), rowCount(), rowCount()); QDomElement category = m_doc.elementsByTagName("qa").at(0).toElement().elementsByTagName("category").at(0).toElement(); QDomElement rule = category.appendChild(m_doc.createElement("rule")).toElement(); rule.appendChild(m_doc.createElement(ruleTagNames[Source])); rule.appendChild(m_doc.createElement(ruleTagNames[FalseFriend])); endInsertRows(); return index(m_entries.count() - 1); } void QaModel::removeRow(const QModelIndex& rowIndex) { //TODO optimize for contiguous selections beginRemoveRows(QModelIndex(), rowIndex.row(), rowIndex.row()); QDomElement category = m_doc.elementsByTagName("qa").at(0).toElement().elementsByTagName("category").at(0).toElement(); category.removeChild(m_entries.at(rowIndex.row())); endRemoveRows(); } Qt::ItemFlags QaModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; } bool QaModel::setData(const QModelIndex& item, const QVariant& value, int role) { if (role != Qt::DisplayRole && role != Qt::EditRole) return false; QDomElement entry = m_entries.at(item.row()).toElement(); QDomNodeList sources = entry.elementsByTagName(ruleTagNames[item.column()]); QStringList newSources = value.toString().split('\n'); while (sources.size() < newSources.size()) entry.insertAfter(m_doc.createElement(ruleTagNames[item.column()]), sources.at(sources.size() - 1)); while (sources.size() > newSources.size()) entry.removeChild(sources.at(sources.size() - 1)); for (int i = 0; i < sources.size(); i++) setText(sources.at(i).toElement(), newSources.at(i)); emit dataChanged(item, item); return true; } diff --git a/src/tm/tmscanapi.h b/src/tm/tmscanapi.h index 3d6a37e..addcff9 100644 --- a/src/tm/tmscanapi.h +++ b/src/tm/tmscanapi.h @@ -1,72 +1,72 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef SCANAPI_H #define SCANAPI_H #include +#include #include -#include #include #include bool dragIsAcceptable(const QList& urls); QString shorterFilePath(const QString path); namespace TM { class ScanJob; class ScanJobFeedingBack; void purgeMissingFilesFromTM(const QStringList& urls, const QString& dbName); ///wrapper. returns gross number of jobs started int scanRecursive(const QStringList& urls, const QString& dbName); class RecursiveScanJob: public KJob { Q_OBJECT public: explicit RecursiveScanJob(const QString& dbName, QObject* parent = nullptr); void setJobs(const QVector& jobs); void start() override; public slots: void scanJobFinished(ScanJobFeedingBack*); void scanJobDestroyed(); protected: bool doKill() override; private: QString m_dbName; - QTime m_time; + QElapsedTimer m_time; QVector m_jobs; qulonglong m_destroyedJobs = 0; }; } #endif diff --git a/src/tm/tmtab.cpp b/src/tm/tmtab.cpp index a244f12..91d9103 100644 --- a/src/tm/tmtab.cpp +++ b/src/tm/tmtab.cpp @@ -1,795 +1,795 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "tmtab.h" #include "lokalize_debug.h" #include "ui_queryoptions.h" #include "project.h" #include "dbfilesmodel.h" #include "tmscanapi.h" #include "qaview.h" #include "prefs_lokalize.h" #include "jobs.h" #include "fastsizehintitemdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) && defined(QStringLiteral) #undef QStringLiteral #define QStringLiteral QLatin1String #endif using namespace TM; //static int BIG_COUNTER=0; //TODO do things for case when user explicitly wants to find & accel mark //BEGIN TMDBModel TMDBModel::TMDBModel(QObject* parent) : QSqlQueryModel(parent) , m_queryType(WordOrder) , m_totalResultCount(0) { setHeaderData(TMDBModel::Source, Qt::Horizontal, i18nc("@title:column Original text", "Source")); setHeaderData(TMDBModel::Target, Qt::Horizontal, i18nc("@title:column Text in target language", "Target")); setHeaderData(TMDBModel::Context, Qt::Horizontal, i18nc("@title:column", "Context")); setHeaderData(TMDBModel::Filepath, Qt::Horizontal, i18nc("@title:column", "File")); setHeaderData(TMDBModel::TransationStatus, Qt::Horizontal, i18nc("@title:column", "Translation Status")); } void TMDBModel::setDB(const QString& str) { m_dbName = str; QString sourceLangCode = DBFilesModel::instance()->m_configurations.value(str).sourceLangCode; QString targetLangCode = DBFilesModel::instance()->m_configurations.value(str).targetLangCode; if (sourceLangCode.length()) setHeaderData(TMDBModel::Source, Qt::Horizontal, QString(i18nc("@title:column Original text", "Source") + QStringLiteral(": ") + sourceLangCode)); if (targetLangCode.length()) setHeaderData(TMDBModel::Target, Qt::Horizontal, QString(i18nc("@title:column Text in target language", "Target") + QStringLiteral(": ") + targetLangCode)); } void TMDBModel::setQueryType(int type) { m_queryType = (QueryType)type; } void TMDBModel::setFilter(const QString& source, const QString& target, bool invertSource, bool invertTarget, const QString& filemask ) { QString escapedSource(source); escapedSource.replace('\'', QStringLiteral("''")); QString escapedTarget(target); escapedTarget.replace('\'', QStringLiteral("''")); QString invertSourceStr; if (invertSource) invertSourceStr = QStringLiteral("NOT "); QString invertTargetStr; if (invertTarget) invertTargetStr = QStringLiteral("NOT "); QString escapedFilemask(filemask); escapedFilemask.replace('\'', QStringLiteral("''")); QString sourceQuery; QString targetQuery; QString fileQuery; if (m_queryType == SubStr) { escapedSource.replace('%', QStringLiteral("\b%")); escapedSource.replace('_', QStringLiteral("\b_")); escapedTarget.replace('%', QStringLiteral("\b%")); escapedTarget.replace('_', QStringLiteral("\b_")); if (!escapedSource.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") + invertSourceStr + QStringLiteral("LIKE '%") + escapedSource + QStringLiteral("%' ESCAPE '\b' "); if (!escapedTarget.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") + invertTargetStr + QStringLiteral("LIKE '%") + escapedTarget + QStringLiteral("%' ESCAPE '\b' "); } else if (m_queryType == WordOrder) { /*escapedSource.replace('%',"\b%");escapedSource.replace('_',"\b_"); escapedTarget.replace('%',"\b%");escapedTarget.replace('_',"\b_");*/ QRegExp wre(QStringLiteral("\\W")); QStringList sourceList = escapedSource.split(wre, QString::SkipEmptyParts); QStringList targetList = escapedTarget.split(wre, QString::SkipEmptyParts); if (!sourceList.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") + invertSourceStr + QStringLiteral("LIKE '%") + sourceList.join(QStringLiteral("%' AND source_strings.source ") + invertSourceStr + QStringLiteral("LIKE '%")) + QStringLiteral("%' "); if (!targetList.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") + invertTargetStr + QStringLiteral("LIKE '%") + targetList.join(QStringLiteral("%' AND target_strings.target ") + invertTargetStr + QStringLiteral("LIKE '%")) + QStringLiteral("%' "); } else { if (!escapedSource.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") + invertSourceStr + QStringLiteral("GLOB '") + escapedSource + QStringLiteral("' "); if (!escapedTarget.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") + invertTargetStr + QStringLiteral("GLOB '") + escapedTarget + QStringLiteral("' "); } if (!filemask.isEmpty()) fileQuery = QStringLiteral("AND files.path GLOB '") + escapedFilemask + QStringLiteral("' "); QString fromPart = QStringLiteral("FROM main JOIN source_strings ON (source_strings.id=main.source) " "JOIN target_strings ON (target_strings.id=main.target), files " "WHERE files.id=main.file ") + sourceQuery + targetQuery + fileQuery; ExecQueryJob* job = new ExecQueryJob(QStringLiteral( "SELECT source_strings.source, target_strings.target, " "main.ctxt, files.path, " "source_strings.source_accel, target_strings.target_accel, main.bits ") + fromPart, m_dbName, &m_dbOperationMutex); connect(job, &ExecQueryJob::done, this, &TMDBModel::slotQueryExecuted); threadPool()->start(job); job = new ExecQueryJob(QStringLiteral("SELECT count(*) ") + fromPart, m_dbName, &m_dbOperationMutex); connect(job, &ExecQueryJob::done, this, &TMDBModel::slotQueryExecuted); threadPool()->start(job); m_totalResultCount = 0; } void TMDBModel::slotQueryExecuted(ExecQueryJob* job) { job->deleteLater(); m_dbOperationMutex.lock(); if (job->query->lastQuery().startsWith(QLatin1String("SELECT count(*) "))) { m_totalResultCount = job->query->next() ? job->query->value(0).toInt() : -1; m_dbOperationMutex.unlock(); emit finalResultCountFetched(m_totalResultCount); return; } query().finish(); query().clear(); setQuery(*(job->query)); m_dbOperationMutex.unlock(); emit resultsFetched(); } bool TMDBModel::rowIsApproved(int row) const { bool ok; qlonglong bits = record(row).value(TMDBModel::_Bits).toLongLong(&ok); return !(ok && bits & 4); } int TMDBModel::translationStatus(const QModelIndex& item) const { //QMutexLocker locker(&m_dbOperationMutex); if (QSqlQueryModel::data(item.sibling(item.row(), Target), Qt::DisplayRole).toString().isEmpty()) return 2; return int(!rowIsApproved(item.row())); } #define TM_DELIMITER '\v' QVariant TMDBModel::data(const QModelIndex& item, int role) const { //QMutexLocker locker(&m_dbOperationMutex); bool doHtml = (role == FastSizeHintItemDelegate::HtmlDisplayRole); if (doHtml) role = Qt::DisplayRole; else if (role == Qt::FontRole && item.column() == TMDBModel::Target) { //TODO Qt::ForegroundRole -- brush for orphaned entries QFont font = QApplication::font(); font.setItalic(!rowIsApproved(item.row())); return font; } else if (role == FullPathRole && item.column() == TMDBModel::Filepath) return QSqlQueryModel::data(item, Qt::DisplayRole); else if (role == TransStateRole) return translationStatus(item); QVariant result = QSqlQueryModel::data(item, role); /* if (role==Qt::SizeHintRole && !result.isValid()) BIG_COUNTER++;*/ if (role != Qt::DisplayRole) return result; if (item.column() == TMDBModel::Context) { //context QString r = result.toString(); int pos = r.indexOf(TM_DELIMITER); if (pos != -1) result = r.remove(pos, r.size()); } else if (item.column() < TMDBModel::Context && !record(item.row()).isNull(TMDBModel::_SourceAccel + item.column())) { //source, target const QVariant& posVar = record(item.row()).value(TMDBModel::_SourceAccel + item.column()); int pos = -1; bool ok = false; if (posVar.isValid()) pos = posVar.toInt(&ok); if (ok && pos != -1) { QString r = result.toString(); r.insert(pos, Project::instance()->accel()); result = r; } } else if (item.column() == TMDBModel::Filepath) { return shorterFilePath(result.toString()); } else if (item.column() == TMDBModel::TransationStatus) { - static QString statuses[] = {i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"), - i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"), - i18nc("@info:status", "Untranslated") - }; + static const QString statuses[] = {i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"), + i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"), + i18nc("@info:status", "Untranslated") + }; return statuses[translationStatus(item)]; } if (doHtml && item.column() < TMDBModel::Context) return convertToHtml(result.toString(), item.column() == TMDBModel::Target && !rowIsApproved(item.row())); else return result; } //END TMDBModel //BEGIN TMResultsSortFilterProxyModel class TMResultsSortFilterProxyModel: public QSortFilterProxyModel { public: TMResultsSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} void setRules(const QVector& rules); void fetchMore(const QModelIndex& parent) override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; private: QVector m_rules; mutable QMap m_matchingRulesForSourceRow; //mutable QMap > m_highlightDataForSourceRow; }; bool TMResultsSortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { if (left.column() == TMDBModel::TransationStatus) { int l = left.data(TMDBModel::TransStateRole).toInt(); int r = right.data(TMDBModel::TransStateRole).toInt(); return l < r; } return QSortFilterProxyModel::lessThan(left, right); } void TMResultsSortFilterProxyModel::fetchMore(const QModelIndex& parent) { int oldSourceRowCount = sourceModel()->rowCount(); int oldRowCount = rowCount(); QSortFilterProxyModel::fetchMore(parent); if (m_rules.isEmpty()) return; while (oldRowCount == rowCount()) { QSortFilterProxyModel::fetchMore(parent); if (sourceModel()->rowCount() == oldSourceRowCount) break; oldSourceRowCount = sourceModel()->rowCount(); } qCDebug(LOKALIZE_LOG) << "row count" << sourceModel()->rowCount() << " filtered:" << rowCount(); emit layoutChanged(); } void TMResultsSortFilterProxyModel::setRules(const QVector& rules) { m_rules = rules; m_matchingRulesForSourceRow.clear(); invalidateFilter(); } QVariant TMResultsSortFilterProxyModel::data(const QModelIndex& index, int role) const { QVariant result = QSortFilterProxyModel::data(index, role); if (m_rules.isEmpty() || role != FastSizeHintItemDelegate::HtmlDisplayRole) return result; if (index.column() != TMDBModel::Source && index.column() != TMDBModel::Target) return result; int source_row = mapToSource(index).row(); QString string = result.toString(); QVector regExps; if (index.column() == TMDBModel::Source) regExps = m_rules[m_matchingRulesForSourceRow[source_row]].sources; else regExps = m_rules[m_matchingRulesForSourceRow[source_row]].falseFriends; foreach (const QRegExp& re, regExps) { int pos = re.indexIn(string); if (pos != -1) return string.replace(pos, re.matchedLength(), QStringLiteral("") + re.cap(0) + QStringLiteral("")); } //StartLen sl=m_highlightDataForSourceRow.value(source_row).at(index.column()); return result; } bool TMResultsSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (m_rules.isEmpty()) return true; QString source = sourceModel()->index(source_row, TMDBModel::Source, source_parent).data().toString(); QString target = sourceModel()->index(source_row, TMDBModel::Target, source_parent).data().toString(); static QVector dummy_positions; int i = findMatchingRule(m_rules, source, target, dummy_positions); bool accept = (i != -1); if (accept) m_matchingRulesForSourceRow[source_row] = i; return accept; } //END TMResultsSortFilterProxyModel class QueryStylesModel: public QStringListModel { public: explicit QueryStylesModel(QObject* parent = nullptr); QVariant data(const QModelIndex& item, int role) const override; }; QueryStylesModel::QueryStylesModel(QObject* parent): QStringListModel(parent) { setStringList(QStringList(i18n("Substring")) << i18n("Google-like") << i18n("Wildcard")); } QVariant QueryStylesModel::data(const QModelIndex& item, int role) const { if (role == Qt::ToolTipRole) { - static QString tooltips[] = {i18n("Case insensitive"), - i18n("Space is AND operator. Case insensitive."), - i18n("Shell globs (* and ?). Case sensitive.") - }; + static const QString tooltips[] = {i18n("Case insensitive"), + i18n("Space is AND operator. Case insensitive."), + i18n("Shell globs (* and ?). Case sensitive.") + }; return tooltips[item.row()]; } return QStringListModel::data(item, role); } //BEGIN TMWindow TMTab::TMTab(QWidget *parent) : LokalizeSubwindowBase2(parent) , m_proxyModel(new TMResultsSortFilterProxyModel(this)) , m_partToAlsoTryLater(DocPosition::UndefPart) , m_dbusId(-1) { //setCaption(i18nc("@title:window","Translation Memory"),false); setWindowTitle(i18nc("@title:window", "Translation Memory")); setAcceptDrops(true); ui_queryOptions = new Ui_QueryOptions; QWidget* w = new QWidget(this); ui_queryOptions->setupUi(w); setCentralWidget(w); ui_queryOptions->queryLayout->setStretchFactor(ui_queryOptions->mainQueryLayout, 42); connect(ui_queryOptions->querySource, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->queryTarget, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->filemask, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->doFind, &QPushButton::clicked, this, &TMTab::performQuery); connect(ui_queryOptions->doUpdateTM, &QPushButton::clicked, this, &TMTab::updateTM); QShortcut* sh = new QShortcut(Qt::CTRL + Qt::Key_L, this); connect(sh, &QShortcut::activated, ui_queryOptions->querySource, QOverload<>::of(&QLineEdit::setFocus)); setFocusProxy(ui_queryOptions->querySource); QTreeView* view = ui_queryOptions->treeView; view->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* a = new QAction(i18n("Copy source to clipboard"), view); a->setShortcut(Qt::CTRL + Qt::Key_S); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::copySource); view->addAction(a); a = new QAction(i18n("Copy target to clipboard"), view); a->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::copyTarget); view->addAction(a); a = new QAction(i18n("Open file"), view); a->setShortcut(QKeySequence(Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::openFile); connect(view, &QTreeView::activated, this, &TMTab::openFile); view->addAction(a); //view->addAction(KStandardAction::copy(this),this,SLOT(),this); //QKeySequence::Copy? //QShortcut* shortcut = new QShortcut(Qt::CTRL + Qt::Key_P,view,0,0,Qt::WidgetWithChildrenShortcut); //connect(shortcut,SIGNAL(activated()), this, SLOT(copyText())); m_model = new TMDBModel(this); m_model->setDB(Project::instance()->projectID()); m_proxyModel->setDynamicSortFilter(true); m_proxyModel->setSourceModel(m_model); view->setModel(m_proxyModel); view->sortByColumn(TMDBModel::Filepath, Qt::AscendingOrder); view->setSortingEnabled(true); view->setColumnHidden(TMDBModel::_SourceAccel, true); view->setColumnHidden(TMDBModel::_TargetAccel, true); view->setColumnHidden(TMDBModel::_Bits, true); QVector singleLineColumns(TMDBModel::ColumnCount, false); singleLineColumns[TMDBModel::Filepath] = true; singleLineColumns[TMDBModel::TransationStatus] = true; singleLineColumns[TMDBModel::Context] = true; QVector richTextColumns(TMDBModel::ColumnCount, false); richTextColumns[TMDBModel::Source] = true; richTextColumns[TMDBModel::Target] = true; view->setItemDelegate(new FastSizeHintItemDelegate(this, singleLineColumns, richTextColumns)); connect(m_model, &TMDBModel::resultsFetched, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_model, &TMDBModel::modelReset, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); //connect(m_model,SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),view->itemDelegate(),SLOT(reset())); connect(m_proxyModel, &TMResultsSortFilterProxyModel::layoutChanged, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_proxyModel, &TMResultsSortFilterProxyModel::layoutChanged, this, &TMTab::displayTotalResultCount); connect(m_model, &TMDBModel::resultsFetched, this, &TMTab::handleResults); connect(m_model, &TMDBModel::finalResultCountFetched, this, &TMTab::displayTotalResultCount); ui_queryOptions->queryStyle->setModel(new QueryStylesModel(this)); connect(ui_queryOptions->queryStyle, QOverload::of(&KComboBox::currentIndexChanged), m_model, &TMDBModel::setQueryType); ui_queryOptions->dbName->setModel(DBFilesModel::instance()); ui_queryOptions->dbName->setRootModelIndex(DBFilesModel::instance()->rootIndex()); int pos = ui_queryOptions->dbName->findData(Project::instance()->projectID(), DBFilesModel::NameRole); if (pos >= 0) ui_queryOptions->dbName->setCurrentIndex(pos); connect(ui_queryOptions->dbName, QOverload::of(&QComboBox::currentIndexChanged), m_model, &TMDBModel::setDB); //connect(ui_queryOptions->dbName, SIGNAL(activated(QString)), this, SLOT(performQuery())); //BEGIN resizeColumnToContents static const int maxInitialWidths[4] = {QGuiApplication::primaryScreen()->availableGeometry().width() / 3, QGuiApplication::primaryScreen()->availableGeometry().width() / 3, 50, 200}; int column = sizeof(maxInitialWidths) / sizeof(int); while (--column >= 0) view->setColumnWidth(column, maxInitialWidths[column]); //END resizeColumnToContents int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); setXMLFile(QStringLiteral("translationmemoryrui.rc"), true); setUpdatedXMLFile(); dbusObjectPath(); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* tm = new KActionCategory(i18nc("@title actions category", "Translation Memory"), ac); action = tm->addAction(QStringLiteral("tools_tm_manage"), Project::instance(), SLOT(showTMManager())); action->setText(i18nc("@action:inmenu", "Manage translation memories")); m_qaView = new QaView(this); m_qaView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_qaView); tm->addAction(QStringLiteral("showqa_action"), m_qaView->toggleViewAction()); connect(m_qaView, &QaView::rulesChanged, this, QOverload<>::of(&TMTab::setQAMode)); connect(m_qaView->toggleViewAction(), &QAction::toggled, this, QOverload::of(&TMTab::setQAMode)); KConfig config; KConfigGroup cg(&config, "MainWindow"); view->header()->restoreState(QByteArray::fromBase64(cg.readEntry("TMSearchResultsHeaderState", QByteArray()))); } TMTab::~TMTab() { KConfig config; KConfigGroup cg(&config, "MainWindow"); cg.writeEntry("TMSearchResultsHeaderState", ui_queryOptions->treeView->header()->saveState().toBase64()); ids.removeAll(m_dbusId); delete ui_queryOptions; } void TMTab::updateTM() { scanRecursive(QStringList(Project::instance()->poDir()), Project::instance()->projectID()); if (Settings::deleteFromTMOnMissing()) { RemoveMissingFilesJob* job = new RemoveMissingFilesJob(Project::instance()->projectID()); TM::threadPool()->start(job, REMOVEMISSINGFILES); } } void TMTab::performQuery() { if (ui_queryOptions->dbName->currentText().isEmpty()) { int pos = ui_queryOptions->dbName->findData(Project::instance()->projectID(), DBFilesModel::NameRole); if (pos >= 0) ui_queryOptions->dbName->setCurrentIndex(pos); //m_model->setDB(Project::instance()->projectID()); } m_model->m_dbOperationMutex.lock(); m_model->setFilter(ui_queryOptions->querySource->text(), ui_queryOptions->queryTarget->text(), ui_queryOptions->invertSource->isChecked(), ui_queryOptions->invertTarget->isChecked(), ui_queryOptions->filemask->text() ); m_model->m_dbOperationMutex.unlock(); QApplication::setOverrideCursor(Qt::BusyCursor); } void TMTab::handleResults() { QApplication::restoreOverrideCursor(); QString filemask = ui_queryOptions->filemask->text(); //ui_queryOptions->regexSource->text(),ui_queryOptions->regexTarget->text() m_model->m_dbOperationMutex.lock(); int rowCount = m_model->rowCount(); m_model->m_dbOperationMutex.unlock(); if (rowCount == 0) { qCDebug(LOKALIZE_LOG) << "m_model->rowCount()==0"; //try harder if (m_partToAlsoTryLater != DocPosition::UndefPart) { if (m_partToAlsoTryLater == DocPosition::Comment) { QString text = ui_queryOptions->queryTarget->text(); if (text.isEmpty()) text = ui_queryOptions->querySource->text(); if (text.isEmpty()) m_partToAlsoTryLater = DocPosition::UndefPart; else findGuiText(text); return; } QLineEdit* const source_target_query[] = {ui_queryOptions->queryTarget, ui_queryOptions->querySource}; source_target_query[m_partToAlsoTryLater == DocPosition::Source]->setText(source_target_query[m_partToAlsoTryLater != DocPosition::Source]->text()); source_target_query[m_partToAlsoTryLater != DocPosition::Source]->clear(); m_partToAlsoTryLater = ui_queryOptions->filemask->text().isEmpty() ? DocPosition::UndefPart : DocPosition::Comment; //leave a note that we should also try w/o package if the current one doesn't succeed return performQuery(); } if (!filemask.isEmpty() && !filemask.contains('*')) { ui_queryOptions->filemask->setText('*' + filemask + '*'); return performQuery(); } } qCDebug(LOKALIZE_LOG) << "=DocPosition::UndefPart"; m_partToAlsoTryLater = DocPosition::UndefPart; ui_queryOptions->treeView->setFocus(); } void TMTab::displayTotalResultCount() { m_model->m_dbOperationMutex.lock(); int total = m_model->totalResultCount(); int filtered = m_proxyModel->rowCount(); if (filtered == m_model->rowCount()) statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1", total)); else statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1 (%2)", filtered, total)); m_model->m_dbOperationMutex.unlock(); } static void copy(Ui_QueryOptions* ui_queryOptions, int column) { QApplication::clipboard()->setText(ui_queryOptions->treeView->currentIndex().sibling(ui_queryOptions->treeView->currentIndex().row(), column).data().toString()); } void TMTab::copySource() { copy(ui_queryOptions, TMDBModel::Source); } void TMTab::copyTarget() { copy(ui_queryOptions, TMDBModel::Target); } void TMTab::openFile() { QModelIndex item = ui_queryOptions->treeView->currentIndex(); if (Settings::deleteFromTMOnMissing()) { //Check if the file exists and delete it if it doesn't QString filePath = item.sibling(item.row(), TMDBModel::Filepath).data(Qt::UserRole).toString(); if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(filePath, ui_queryOptions->dbName->currentText()); TM::threadPool()->start(job, REMOVEFILE); KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", filePath)); return performQuery();//We relaunch the query } } emit fileOpenRequested(item.sibling(item.row(), TMDBModel::Filepath).data(Qt::UserRole).toString(), item.sibling(item.row(), TMDBModel::Source).data().toString(), item.sibling(item.row(), TMDBModel::Context).data().toString(), true); } void TMTab::setQAMode() { return setQAMode(true); } void TMTab::setQAMode(bool enable) { static_cast(ui_queryOptions->treeView->itemDelegate())->reset(); if (!enable) { m_proxyModel->setRules(QVector()); return; } m_proxyModel->setRules(m_qaView->rules()); /*QDomElement docElem = m_categories.at(0).toElement(); QDomNode n = docElem.firstChildElement(); while(!n.isNull()) { QDomElement e = n.toElement(); qCDebug(LOKALIZE_LOG) << e.tagName(); n = n.nextSiblingElement(); }*/ performQuery(); } //END TMWindow #if 0 bool QueryResultDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { qCWarning(LOKALIZE_LOG) << "QEvent" << event; if (event->type() == QEvent::Shortcut) { qCWarning(LOKALIZE_LOG) << "QEvent::Shortcut" << index.data().canConvert(QVariant::String); if (static_cast(event)->key().matches(QKeySequence::Copy) && index.data().canConvert(QVariant::String)) { QApplication::clipboard()->setText(index.data().toString()); qCWarning(LOKALIZE_LOG) << "index.data().toString()"; } } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mEvent = static_cast(event); if (mEvent->button() == Qt::MidButton) { } } else if (event->type() == QEvent::KeyPress) { QKeyEvent* kEvent = static_cast(event); if (kEvent->key() == Qt::Key_Return) { if (kEvent->modifiers() == Qt::NoModifier) { } } } else return false; event->accept(); return true; } #endif void TMTab::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMTab::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } #include "translationmemoryadaptor.h" //BEGIN DBus interface QList TMTab::ids; QString TMTab::dbusObjectPath() { const QString TM_PATH = QStringLiteral("/ThisIsWhatYouWant/TranslationMemory/"); if (m_dbusId == -1) { new TranslationMemoryAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(TM_PATH + QString::number(m_dbusId), this); } return TM_PATH + QString::number(m_dbusId); } void TMTab::lookup(QString source, QString target) { source.remove(Project::instance()->accel()); target.remove(Project::instance()->accel()); ui_queryOptions->querySource->setText(source); ui_queryOptions->queryTarget->setText(target); ui_queryOptions->invertSource->setChecked(false); ui_queryOptions->invertTarget->setChecked(false); ui_queryOptions->queryStyle->setCurrentIndex(TMDBModel::SubStr); performQuery(); } // void TMTab::lookup(DocPosition::Part part, QString text) // { // lookup(part==DocPosition::Source?text:QString(),part==DocPosition::Target?text:QString()); // } bool TMTab::findGuiTextPackage(QString text, QString package) { qCWarning(LOKALIZE_LOG) << package << text; QLineEdit* const source_target_query[] = {ui_queryOptions->queryTarget, ui_queryOptions->querySource}; static const DocPosition::Part source_target[] = {DocPosition::Target, DocPosition::Source}; QTextCodec* latin1 = QTextCodec::codecForMib(4); DocPosition::Part tryNowPart = source_target[latin1->canEncode(text)]; m_partToAlsoTryLater = source_target[tryNowPart == DocPosition::Target]; text.remove(Project::instance()->accel()); ui_queryOptions->querySource->clear(); ui_queryOptions->queryTarget->clear(); source_target_query[tryNowPart == DocPosition::Source]->setText(text); ui_queryOptions->invertSource->setChecked(false); ui_queryOptions->invertTarget->setChecked(false); if (!package.isEmpty()) package = '*' + package + '*'; ui_queryOptions->filemask->setText(package); ui_queryOptions->queryStyle->setCurrentIndex(TMDBModel::Glob); performQuery(); return true; } //END DBus interface diff --git a/src/tm/tmview.cpp b/src/tm/tmview.cpp index 1e3735a..0bae030 100644 --- a/src/tm/tmview.cpp +++ b/src/tm/tmview.cpp @@ -1,1022 +1,1022 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "tmview.h" #include "lokalize_debug.h" #include "jobs.h" #include "tmscanapi.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "dbfilesmodel.h" #include "diff.h" #include "xlifftextedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef NDEBUG #undef NDEBUG #endif #define DEBUG using namespace TM; struct DiffInfo { DiffInfo(int reserveSize); QString diffClean; QString old; //Formatting info: QByteArray diffIndex; //Map old string-->d.diffClean QVector old2DiffClean; }; DiffInfo::DiffInfo(int reserveSize) { diffClean.reserve(reserveSize); old.reserve(reserveSize); diffIndex.reserve(reserveSize); old2DiffClean.reserve(reserveSize); } /** * 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 (M appears afterwards) */ static DiffInfo getDiffInfo(const QString& diff) { DiffInfo d(diff.size()); QChar sep('{'); char state = '0'; //walk through diff string char-by-char //calculate old and others int pos = -1; while (++pos < diff.size()) { if (diff.at(pos) == sep) { if (diff.indexOf(QLatin1String("{KBABELDEL}"), pos) == pos) { state = '-'; pos += 10; } else if (diff.indexOf(QLatin1String("{KBABELADD}"), pos) == pos) { state = '+'; pos += 10; } else if (diff.indexOf(QLatin1String("{/KBABEL"), pos) == pos) { state = '0'; pos += 11; } } else { if (state != '+') { d.old.append(diff.at(pos)); d.old2DiffClean.append(d.diffIndex.count()); } d.diffIndex.append(state); d.diffClean.append(diff.at(pos)); } } return d; } void TextBrowser::mouseDoubleClickEvent(QMouseEvent* event) { QTextBrowser::mouseDoubleClickEvent(event); QString sel = textCursor().selectedText(); if (!(sel.isEmpty() || sel.contains(' '))) emit textInsertRequested(sel); } TMView::TMView(QWidget* parent, Catalog* catalog, const QVector& actions_insert, const QVector& actions_remove) : QDockWidget(i18nc("@title:window", "Translation Memory"), parent) , m_browser(new TextBrowser(this)) , m_catalog(catalog) , m_currentSelectJob(0) , m_actions_insert(actions_insert) , m_actions_remove(actions_remove) , m_normTitle(i18nc("@title:window", "Translation Memory")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_isBatching(false) , m_markAsFuzzy(false) { setObjectName(QStringLiteral("TMView")); setWidget(m_browser); m_browser->document()->setDefaultStyleSheet(QStringLiteral("p.close_match { font-weight:bold; }")); m_browser->viewport()->setBackgroundRole(QPalette::Window); QTimer::singleShot(0, this, &TMView::initLater); connect(m_catalog, QOverload::of(&Catalog::signalFileLoaded), this, &TMView::slotFileLoaded); } TMView::~TMView() { #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->tryTake(m_jobs.at(i)); #endif } void TMView::initLater() { setAcceptDrops(true); int i = m_actions_insert.size(); while (--i >= 0) { connect(m_actions_insert.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); }); } i = m_actions_remove.size(); while (--i >= 0) { connect(m_actions_remove.at(i), &QAction::triggered, this, [this, i] { slotRemoveSuggestion(i); }); } setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); DBFilesModel::instance(); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &TMView::textInsertRequested); connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &TMView::contextMenu); //TODO ? kdisplayPaletteChanged // connect(KGlobalSettings::self(),,SIGNAL(kdisplayPaletteChanged()),this,SLOT(slotPaletteChanged())); } void TMView::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMView::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } void TMView::slotFileLoaded(const QString& filePath) { const QString& pID = Project::instance()->projectID(); if (Settings::scanToTMOnOpen()) TM::threadPool()->start(new ScanJob(filePath, pID), SCAN); if (!Settings::prefetchTM() && !m_isBatching) return; m_cache.clear(); #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->tryTake(m_jobs.at(i)); #endif m_jobs.clear(); DocPosition pos; while (switchNext(m_catalog, pos)) { if (!m_catalog->isEmpty(pos.entry) && m_catalog->isApproved(pos.entry)) continue; SelectJob* j = initSelectJob(m_catalog, pos, pID); connect(j, &SelectJob::done, this, &TMView::slotCacheSuggestions); m_jobs.append(j); } //dummy job for the finish indication BatchSelectFinishedJob* m_seq = new BatchSelectFinishedJob(this); connect(m_seq, &BatchSelectFinishedJob::done, this, &TMView::slotBatchSelectDone); TM::threadPool()->start(m_seq, BATCHSELECTFINISHED); m_jobs.append(m_seq); } void TMView::slotCacheSuggestions(SelectJob* job) { m_jobs.removeAll(job); qCDebug(LOKALIZE_LOG) << job->m_pos.entry; if (job->m_pos.entry == m_pos.entry) slotSuggestionsCame(job); m_cache[DocPos(job->m_pos)] = job->m_entries.toVector(); } void TMView::slotBatchSelectDone() { m_jobs.clear(); if (!m_isBatching) return; bool insHappened = false; DocPosition pos; while (switchNext(m_catalog, pos)) { if (!(m_catalog->isEmpty(pos.entry) || !m_catalog->isApproved(pos.entry)) ) continue; const QVector& suggList = m_cache.value(DocPos(pos)); if (suggList.isEmpty()) continue; const TMEntry& entry = suggList.first(); if (entry.score < 9900) //hacky continue; { bool forceFuzzy = (suggList.size() > 1 && suggList.at(1).score >= 10000) || entry.score < 10000; bool ctxtMatches = entry.score == 1001; if (!m_catalog->isApproved(pos.entry)) { ///m_catalog->push(new DelTextCmd(m_catalog,pos,m_catalog->msgstr(pos))); removeTargetSubstring(m_catalog, pos, 0, m_catalog->targetWithTags(pos).string.size()); if (ctxtMatches || !(m_markAsFuzzy || forceFuzzy)) SetStateCmd::push(m_catalog, pos, true); } else if ((m_markAsFuzzy && !ctxtMatches) || forceFuzzy) { SetStateCmd::push(m_catalog, pos, false); } ///m_catalog->push(new InsTextCmd(m_catalog,pos,entry.target)); insertCatalogString(m_catalog, pos, entry.target, 0); if (Q_UNLIKELY(m_pos.entry == pos.entry && pos.form == m_pos.form)) emit refreshRequested(); } if (!insHappened) { insHappened = true; m_catalog->beginMacro(i18nc("@item Undo action", "Batch translation memory filling")); } } QString msg = i18nc("@info", "Batch translation has been completed."); if (insHappened) m_catalog->endMacro(); else { // xgettext: no-c-format msg += ' '; msg += i18nc("@info", "No suggestions with exact matches were found."); } KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation complete"), msg, this); } void TMView::slotBatchTranslate() { m_isBatching = true; m_markAsFuzzy = false; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) return slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotBatchTranslateFuzzy() { m_isBatching = true; m_markAsFuzzy = true; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotNewEntryDisplayed() { return slotNewEntryDisplayed(DocPosition()); } void TMView::slotNewEntryDisplayed(const DocPosition& pos) { if (m_catalog->numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->tryTake(m_currentSelectJob); #endif //update DB //m_catalog->flushUpdateDBBuffer(); //this is called via subscribtion if (pos.entry != -1) m_pos = pos; m_browser->clear(); if (Settings::prefetchTM() && m_cache.contains(DocPos(m_pos))) { QTimer::singleShot(0, this, &TMView::displayFromCache); } m_currentSelectJob = initSelectJob(m_catalog, m_pos); connect(m_currentSelectJob, &TM::SelectJob::done, this, &TMView::slotSuggestionsCame); } void TMView::displayFromCache() { if (m_prevCachePos.entry == m_pos.entry && m_prevCachePos.form == m_pos.form) return; SelectJob* temp = initSelectJob(m_catalog, m_pos, QString(), 0); temp->m_entries = m_cache.value(DocPos(m_pos)).toList(); slotSuggestionsCame(temp); temp->deleteLater(); m_prevCachePos = m_pos; } void TMView::slotSuggestionsCame(SelectJob* j) { - QTime time; - time.start(); +// QTime time; +// time.start(); SelectJob& job = *j; job.deleteLater(); if (job.m_pos.entry != m_pos.entry) return; Catalog& catalog = *m_catalog; if (catalog.numberOfEntries() <= m_pos.entry) return;//because of Qt::QueuedConnection //BEGIN query other DBs handling Project* project = Project::instance(); const QString& projectID = project->projectID(); //check if this is an additional query, from secondary DBs if (job.m_dbName != projectID) { job.m_entries += m_entries; std::sort(job.m_entries.begin(), job.m_entries.end(), std::greater()); const int limit = qMin(Settings::suggCount(), job.m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = job.m_entries.size() - 1; while (i >= 0 && (i >= limit || job.m_entries.last().score < minScore)) { job.m_entries.removeLast(); i--; } } else if (job.m_entries.isEmpty() || job.m_entries.first().score < 8500) { //be careful, as we switched to QDirModel! DBFilesModel& dbFilesModel = *(DBFilesModel::instance()); QModelIndex root = dbFilesModel.rootIndex(); int i = dbFilesModel.rowCount(root); //qCWarning(LOKALIZE_LOG)<<"query other DBs,"<= 0) { const QString& dbName = dbFilesModel.data(dbFilesModel.index(i, 0, root), DBFilesModel::NameRole).toString(); if (projectID != dbName && dbFilesModel.m_configurations.value(dbName).targetLangCode == catalog.targetLangCode()) { SelectJob* j = initSelectJob(m_catalog, m_pos, dbName); connect(j, &SelectJob::done, this, &TMView::slotSuggestionsCame); m_jobs.append(j); } } } //END query other DBs handling m_entries = job.m_entries; const int limit = job.m_entries.size(); if (!limit) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(false); m_browser->clear(); m_entryPositions.clear(); //m_entries=job.m_entries; //m_browser->insertHtml(""); int i = 0; QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); forever { QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); const TMEntry& entry = job.m_entries.at(i); html += (entry.score > 9500) ? QStringLiteral("

    ") : QStringLiteral("

    "); //qCDebug(LOKALIZE_LOG)< 10000 ? 100 : float(entry.score) / 100)); html += QStringLiteral(" "); html += QString(i18ncp("%1 is the number of times this TM entry has been found", "(1 time)", "(%1 times)", entry.hits)); html += QStringLiteral("/ "); //int sourceStartPos=cur.position(); QString result = entry.diff.toHtmlEscaped(); //result.replace("&","&"); //result.replace("<","<"); //result.replace(">",">"); result.replace(QLatin1String("{KBABELADD}"), QStringLiteral("")); result.replace(QLatin1String("{/KBABELADD}"), QLatin1String("")); result.replace(QLatin1String("{KBABELDEL}"), QStringLiteral("")); result.replace(QLatin1String("{/KBABELDEL}"), QLatin1String("")); result.replace(QLatin1String("\\n"), QLatin1String("\\n
    ")); result.replace(QLatin1String("\\n"), QLatin1String("\\n
    ")); html += result; #if 0 cur.insertHtml(result); cur.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, cur.position() - sourceStartPos); CatalogString catStr(entry.diff); catStr.string.remove("{KBABELDEL}"); catStr.string.remove("{/KBABELDEL}"); catStr.string.remove("{KBABELADD}"); catStr.string.remove("{/KBABELADD}"); catStr.tags = entry.source.tags; DiffInfo d = getDiffInfo(entry.diff); int j = catStr.tags.size(); while (--j >= 0) { catStr.tags[j].start = d.old2DiffClean.at(catStr.tags.at(j).start); catStr.tags[j].end = d.old2DiffClean.at(catStr.tags.at(j).end); } insertContent(cur, catStr, job.m_source, false); #endif //str.replace('&',"&"); TODO check html += QLatin1String("
    "); if (Q_LIKELY(i < m_actions_insert.size())) { m_actions_insert.at(i)->setStatusTip(entry.target.string); html += QStringLiteral("[%1] ").arg(m_actions_insert.at(i)->shortcut().toString(QKeySequence::NativeText)); } else html += QLatin1String("[ - ] "); /* QString str(entry.target.string); str.replace('<',"<"); str.replace('>',">"); html+=str; */ cur.insertHtml(html); html.clear(); cur.setCharFormat((entry.score > 9500) ? closeMatchCharFormat : noncloseMatchCharFormat); insertContent(cur, entry.target); m_entryPositions.insert(cur.anchor(), i); html += i ? QStringLiteral("

    ") : QStringLiteral("

    "); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } m_browser->insertHtml(QStringLiteral("")); setUpdatesEnabled(true); // qCWarning(LOKALIZE_LOG)<<"ELA "<document()->blockCount(); } /* void TMView::slotPaletteChanged() { }*/ bool TMView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); //int block1=m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); QMap::iterator block = m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block != m_entryPositions.end() && *block < m_entries.size()) { const TMEntry& tmEntry = m_entries.at(*block); QString file = tmEntry.file; if (file == m_catalog->url()) file = i18nc("File argument in tooltip, when file is current file", "this"); QString tooltip = i18nc("@info:tooltip", "File: %1
    Addition date: %2", file, tmEntry.date.toString(Qt::ISODate)); if (!tmEntry.changeDate.isNull() && tmEntry.changeDate != tmEntry.date) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Last change date: %1", tmEntry.changeDate.toString(Qt::ISODate)); if (!tmEntry.changeAuthor.isEmpty()) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Last change author: %1", tmEntry.changeAuthor); tooltip += i18nc("@info:tooltip on TM entry continues", "
    TM: %1", tmEntry.dbName); if (tmEntry.obsolete) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Is not present in the file anymore"); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } } return QWidget::event(event); } void TMView::removeEntry(const TMEntry& e) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this entry:
    %1
    from translation memory %2?", e.target.string.toHtmlEscaped(), e.dbName), i18nc("@title:window", "Translation Memory Entry Removal"))) { RemoveJob* job = new RemoveJob(e); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVE); } } void TMView::deleteFile(const TMEntry& e, const bool showPopUp) { QString filePath = e.file; if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(e.file, e.dbName); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVEFILE); if (showPopUp) { KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", e.file)); } return; } } void TMView::contextMenu(const QPoint& pos) { int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(pos).anchor()); qCWarning(LOKALIZE_LOG) << block; if (block >= m_entries.size()) return; const TMEntry& e = m_entries.at(block); enum {Remove, RemoveFile, Open}; QMenu popup; popup.addAction(i18nc("@action:inmenu", "Remove this entry"))->setData(Remove); if (e.file != m_catalog->url() && QFile::exists(e.file)) popup.addAction(i18nc("@action:inmenu", "Open file containing this entry"))->setData(Open); else { if (Settings::deleteFromTMOnMissing()) { //Automatic deletion deleteFile(e, true); } else if (!QFile::exists(e.file)) { //Still offer manual deletion if this is not the current file popup.addAction(i18nc("@action:inmenu", "Remove this missing file from TM"))->setData(RemoveFile); } } QAction* r = popup.exec(m_browser->mapToGlobal(pos)); if (!r) return; if (r->data().toInt() == Remove) { removeEntry(e); } else if (r->data().toInt() == Open) { emit fileOpenRequested(e.file, e.source.string, e.ctxt, true); } else if ((r->data().toInt() == RemoveFile) && KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this missing file:
    %1
    from translation memory %2?", e.file, e.dbName), i18nc("@title:window", "Translation Memory Missing File Removal"))) { deleteFile(e, false); } } /** * helper function: * searches to th nearest rxNum or ABBR * clears rxNum if ABBR is found before rxNum */ static int nextPlacableIn(const QString& old, int start, QString& cap) { static QRegExp rxNum(QStringLiteral("[\\d\\.\\%]+")); static QRegExp rxAbbr(QStringLiteral("\\w+")); int numPos = rxNum.indexIn(old, start); // int abbrPos=rxAbbr.indexIn(old,start); int abbrPos = start; //qCWarning(LOKALIZE_LOG)<<"seeing"<= 0) { if ((c++)->isUpper()) break; } abbrPos += rxAbbr.matchedLength(); } int pos = qMin(numPos, abbrPos); if (pos == -1) pos = qMax(numPos, abbrPos); // if (pos==numPos) // cap=rxNum.cap(0); // else // cap=rxAbbr.cap(0); cap = (pos == numPos ? rxNum : rxAbbr).cap(0); //qCWarning(LOKALIZE_LOG)<]*") + Settings::addColor().name() + QLatin1String("[^>]*\">([^>]*)")); QRegExp rxDel(QLatin1String("]*") + Settings::delColor().name() + QLatin1String("[^>]*\">([^>]*)")); //rxAdd.setMinimal(true); //rxDel.setMinimal(true); //first things first int pos = 0; while ((pos = rxDel.indexIn(diff, pos)) != -1) diff.replace(pos, rxDel.matchedLength(), "\tKBABELDEL\t" + rxDel.cap(1) + "\t/KBABELDEL\t"); pos = 0; while ((pos = rxAdd.indexIn(diff, pos)) != -1) diff.replace(pos, rxAdd.matchedLength(), "\tKBABELADD\t" + rxAdd.cap(1) + "\t/KBABELADD\t"); diff.replace(QStringLiteral("<"), QStringLiteral("<")); diff.replace(QStringLiteral(">"), QStringLiteral(">")); //possible enhancement: search for non-translated words in removedSubstrings... //QStringList removedSubstrings; //QStringList addedSubstrings; /* 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 */ DiffInfo d = getDiffInfo(diff); bool sameMarkup = Project::instance()->markup() == entry.markupExpr && !entry.markupExpr.isEmpty(); bool tryMarkup = !entry.target.tags.size() && sameMarkup; //search for changed markup if (tryMarkup) { QRegExp rxMarkup(entry.markupExpr); rxMarkup.setMinimal(true); pos = 0; int replacingPos = 0; while ((pos = rxMarkup.indexIn(d.old, pos)) != -1) { //qCWarning(LOKALIZE_LOG)<<"size"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 0) { QByteArray diffMPart(d.diffIndex.left(len)); int m = diffMPart.indexOf('M'); if (m != -1) diffMPart.truncate(m); #if 0 nono //first goes del, then add. so stop on second del sequence bool seenAdd = false; int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) == '+') seenAdd = true; else if (seenAdd && diffMPart.at(j) == '-') { diffMPart.truncate(j); break; } } #endif //form 'oldMarkup' QString oldMarkup; oldMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '+') oldMarkup.append(d.diffClean.at(j)); } //qCWarning(LOKALIZE_LOG)<<"old"<= 0) d.diffIndex[j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= 0) d.diffIndex[len + j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 500 cases while ((++endPos < d.diffIndex.size()) && (d.diffIndex.at(endPos) == '+') && (-1 != nextPlacableIn(QString(d.diffClean.at(endPos)), 0, _)) ) diffMPart.append('+'); qCDebug(LOKALIZE_LOG) << "diffMPart extended 1" << diffMPart; // if ((pos-1>=0) && (d.old2DiffClean.at(pos)>=0)) // { // qCWarning(LOKALIZE_LOG)<<"d.diffIndex"<= 0) && (d.diffIndex.at(startPos) == '+') //&&(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_)) ) diffMPart.prepend('+'); ++startPos; qCDebug(LOKALIZE_LOG) << "diffMPart extended 2" << diffMPart; if ((diffMPart.contains('-') || diffMPart.contains('+')) && (!diffMPart.contains('M'))) { //form newMarkup QString newMarkup; newMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '-') newMarkup.append(d.diffClean.at(startPos + j)); } if (newMarkup.endsWith(' ')) newMarkup.chop(1); //qCWarning(LOKALIZE_LOG)<<"d.old"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= m_entries.size())) return; const TMEntry& e = m_entries.at(i); removeEntry(e); } void TMView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; CatalogString target = targetAdapted(m_entries.at(i), m_catalog->sourceWithTags(m_pos)); #if 0 QString tmp = target.string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, '*'); qCWarning(LOKALIZE_LOG) << "targetAdapted" << tmp; foreach (InlineTag tag, target.tags) qCWarning(LOKALIZE_LOG) << "tag" << tag.start << tag.end; #endif if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use translation memory suggestion")); QString old = m_catalog->targetWithTags(m_pos).string; if (!old.isEmpty()) { m_pos.offset = 0; //FIXME test! removeTargetSubstring(m_catalog, m_pos, 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_pos, target, 0); if (m_entries.at(i).score > 9900 && !m_catalog->isApproved(m_pos.entry)) SetStateCmd::push(m_catalog, m_pos, true); m_catalog->endMacro(); emit refreshRequested(); } diff --git a/src/xlifftextedit.cpp b/src/xlifftextedit.cpp index fb98b27..42e6993 100644 --- a/src/xlifftextedit.cpp +++ b/src/xlifftextedit.cpp @@ -1,1324 +1,1323 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "xlifftextedit.h" #include "lokalize_debug.h" #include "catalog.h" #include "cmd.h" #include "syntaxhighlighter.h" #include "prefs_lokalize.h" #include "prefs.h" #include "project.h" #include "completionstorage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline static QImage generateImage(const QString& str, const QFont& font) { // im_count++; // QTime a;a.start(); QStyleOptionButton opt; opt.fontMetrics = QFontMetrics(font); opt.text = ' ' + str + ' '; opt.rect = opt.fontMetrics.boundingRect(opt.text).adjusted(0, 0, 5, 5); opt.rect.moveTo(0, 0); QImage result(opt.rect.size(), QImage::Format_ARGB32); result.fill(0);//0xAARRGGBB QPainter painter(&result); QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, &painter); // im_time+=a.elapsed(); // qCWarning(LOKALIZE_LOG)<width() + 2 * frameWidth(); return QSize(w, h); } bool MyCompletionBox::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* e = static_cast(event); if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp) { hide(); return false; } } return KCompletionBox::eventFilter(object, event); } TranslationUnitTextEdit::~TranslationUnitTextEdit() { disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); } TranslationUnitTextEdit::TranslationUnitTextEdit(Catalog* catalog, DocPosition::Part part, QWidget* parent) : KTextEdit(parent) , m_currentUnicodeNumber(0) , m_langUsesSpaces(true) , m_catalog(catalog) , m_part(part) , m_highlighter(new SyntaxHighlighter(this)) , m_enabled(Settings::autoSpellcheck()) , m_completionBox(0) , m_cursorSelectionStart(0) , m_cursorSelectionEnd(0) { setReadOnly(part == DocPosition::Source); setUndoRedoEnabled(false); setAcceptRichText(false); m_highlighter->setActive(m_enabled); setHighlighter(m_highlighter); if (part == DocPosition::Target) { connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); connect(this, &KTextEdit::cursorPositionChanged, this, &TranslationUnitTextEdit::emitCursorPositionChanged); } connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &TranslationUnitTextEdit::fileLoaded); //connect (Project::instance(), &Project::configChanged, this, &TranslationUnitTextEdit::projectConfigChanged); } void TranslationUnitTextEdit::setSpellCheckingEnabled(bool enable) { Settings::setAutoSpellcheck(enable); m_enabled = enable; m_highlighter->setActive(enable); SettingsController::instance()->dirty = true; } void TranslationUnitTextEdit::setVisualizeSeparators(bool enable) { if (enable) { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() | QTextOption::ShowLineAndParagraphSeparators | QTextOption::ShowTabsAndSpaces); document()->setDefaultTextOption(textoption); } else { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() & (~QTextOption::ShowLineAndParagraphSeparators) & (~QTextOption::ShowTabsAndSpaces)); document()->setDefaultTextOption(textoption); } } void TranslationUnitTextEdit::fileLoaded() { QString langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale langLocale(langCode); // First try to use a locale name derived from the language code m_highlighter->setCurrentLanguage(langLocale.name()); // If that fails, try to use the language code directly if (m_highlighter->currentLanguage() != langLocale.name() || m_highlighter->currentLanguage().isEmpty()) { m_highlighter->setCurrentLanguage(langCode); if (m_highlighter->currentLanguage() != langCode && langCode.length() > 2) m_highlighter->setCurrentLanguage(langCode.left(2)); } //"i use an english locale while translating kde pot files from english to hebrew" Bug #181989 Qt::LayoutDirection targetLanguageDirection = Qt::LeftToRight; - static QLocale::Language rtlLanguages[] = {QLocale::Arabic, QLocale::Hebrew, QLocale::Urdu, QLocale::Persian, QLocale::Pashto}; + static const QLocale::Language rtlLanguages[] = {QLocale::Arabic, QLocale::Hebrew, QLocale::Urdu, QLocale::Persian, QLocale::Pashto}; int i = sizeof(rtlLanguages) / sizeof(QLocale::Arabic); while (--i >= 0 && langLocale.language() != rtlLanguages[i]) ; if (i != -1) targetLanguageDirection = Qt::RightToLeft; setLayoutDirection(targetLanguageDirection); if (m_part == DocPosition::Source) return; //"Some language do not need space between words. For example Chinese." - static QLocale::Language noSpaceLanguages[] = {QLocale::Chinese}; + static const QLocale::Language noSpaceLanguages[] = {QLocale::Chinese}; i = sizeof(noSpaceLanguages) / sizeof(QLocale::Chinese); while (--i >= 0 && langLocale.language() != noSpaceLanguages[i]) ; m_langUsesSpaces = (i == -1); } void TranslationUnitTextEdit::reflectApprovementState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool approved = m_catalog->isApproved(m_currentPos.entry); disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); m_highlighter->setApprovementState(approved); m_highlighter->rehighlight(); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); viewport()->setBackgroundRole(approved ? QPalette::Base : QPalette::AlternateBase); if (approved) emit approvedEntryDisplayed(); else emit nonApprovedEntryDisplayed(); bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } void TranslationUnitTextEdit::reflectUntranslatedState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } /** * makes MsgEdit reflect current entry **/ CatalogString TranslationUnitTextEdit::showPos(DocPosition docPosition, const CatalogString& refStr, bool keepCursor) { docPosition.part = m_part; m_currentPos = docPosition; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QString target = catalogString.string; _oldMsgstr = target; //_oldMsgstrAscii=document()->toPlainText(); <-- MOVED THIS TO THE END //BEGIN pos QTextCursor cursor = textCursor(); int pos = cursor.position(); int anchor = cursor.anchor(); //qCWarning(LOKALIZE_LOG)<<"called"<<"pos"<sourceWithTags(docPosition) : refStr); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); _oldMsgstrAscii = document()->toPlainText(); //BEGIN pos QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); if (pos || anchor) { //qCWarning(LOKALIZE_LOG)<<"setting"<blockSignals(true); clear(); QTextCursor c = textCursor(); insertContent(c, catStr, refStr); document()->blockSignals(false); if (m_part == DocPosition::Target) m_highlighter->setSourceString(refStr.string); else //reflectApprovementState() does this for Target m_highlighter->rehighlight(); //explicitly because the signals were disabled } #if 0 struct SearchFunctor { virtual int operator()(const QString& str, int startingPos); }; int SearchFunctor::operator()(const QString& str, int startingPos) { return str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); } struct AlternativeSearchFunctor: public SearchFunctor { int operator()(const QString& str, int startingPos); }; int AlternativeSearchFunctor::operator()(const QString& str, int startingPos) { int tagPos = str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); int diffStartPos = str.indexOf("{KBABEL", startingPos); int diffEndPos = str.indexOf("{/KBABEL", startingPos); int diffPos = qMin(diffStartPos, diffEndPos); if (diffPos == -1) diffPos = qMax(diffStartPos, diffEndPos); int result = qMin(tagPos, diffPos); if (result == -1) result = qMax(tagPos, diffPos); return result; } #endif void insertContent(QTextCursor& cursor, const CatalogString& catStr, const CatalogString& refStr, bool insertText) { //settings for TMView QTextCharFormat chF = cursor.charFormat(); QFont font = cursor.document()->defaultFont(); //font.setWeight(chF.fontWeight()); QMap posToTag; int i = catStr.tags.size(); while (--i >= 0) { //qCDebug(LOKALIZE_LOG)<<"\t"< sourceTagIdToIndex = refStr.tagIdToIndex(); int refTagIndexOffset = sourceTagIdToIndex.size(); i = 0; int prev = 0; while ((i = catStr.string.indexOf(TAGRANGE_IMAGE_SYMBOL, i)) != -1) { #if 0 SearchFunctor nextStopSymbol = AlternativeSearchFunctor(); char state = '0'; while ((i = nextStopSymbol(catStr.string, i)) != -1) { //handle diff display for TMView if (catStr.string.at(i) != TAGRANGE_IMAGE_SYMBOL) { if (catStr.string.at(i + 1) == '/') state = '0'; else if (catStr.string.at(i + 8) == 'D') state = '-'; else state = '+'; continue; } #endif if (insertText) cursor.insertText(catStr.string.mid(prev, i - prev)); else { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, i - prev); cursor.deleteChar();//delete TAGRANGE_IMAGE_SYMBOL to insert it properly } if (!posToTag.contains(i)) { prev = ++i; continue; } int tagIndex = posToTag.value(i); InlineTag tag = catStr.tags.at(tagIndex); QString name = tag.id; QString text; if (tag.type == InlineTag::mrk) text = QStringLiteral("*"); else if (!tag.equivText.isEmpty()) text = tag.equivText; //TODO add number? when? -- right now this is done for gettext qt's 156 mark else text = QString::number(sourceTagIdToIndex.contains(tag.id) ? sourceTagIdToIndex.value(tag.id) : (tagIndex + refTagIndexOffset)); if (tag.start != tag.end) { //qCWarning(LOKALIZE_LOG)<<"b"<resource(QTextDocument::ImageResource, QUrl(name)).isNull()) cursor.document()->addResource(QTextDocument::ImageResource, QUrl(name), generateImage(text, font)); cursor.insertImage(name);//NOTE what if twice the same name? cursor.setCharFormat(chF); prev = ++i; } cursor.insertText(catStr.string.mid(prev)); } void TranslationUnitTextEdit::contentsChanged(int offset, int charsRemoved, int charsAdded) { Q_ASSERT(m_catalog->targetLangCode().length()); Q_ASSERT(Project::instance()->targetLangCode().length()); //qCWarning(LOKALIZE_LOG)<<"contentsChanged. offset"<toPlainText(); if (editTextAscii == _oldMsgstrAscii) { //qCWarning(LOKALIZE_LOG)<<"stopping"<targetWithTags(pos).string; const QStringRef addedText = editText.midRef(offset, charsAdded); //BEGIN XLIFF markup handling //protect from tag removal //TODO use midRef when Qt 4.8 is in distros bool markupRemoved = charsRemoved && target.midRef(offset, charsRemoved).contains(TAGRANGE_IMAGE_SYMBOL); bool markupAdded = charsAdded && addedText.contains(TAGRANGE_IMAGE_SYMBOL); if (markupRemoved || markupAdded) { bool modified = false; CatalogString targetWithTags = m_catalog->targetWithTags(m_currentPos); //special case when the user presses Del w/o selection if (!charsAdded && charsRemoved == 1) { int i = targetWithTags.tags.size(); while (--i >= 0) { if (targetWithTags.tags.at(i).start == offset || targetWithTags.tags.at(i).end == offset) { modified = true; pos.offset = targetWithTags.tags.at(i).start; m_catalog->push(new DelTagCmd(m_catalog, pos)); } } } else if (!markupAdded) { //check if all { plus } tags were selected modified = removeTargetSubstring(offset, charsRemoved, /*refresh*/false); if (modified && charsAdded) m_catalog->push(new InsTextCmd(m_catalog, pos, addedText.toString())); } //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true); if (!modified) { //qCWarning(LOKALIZE_LOG)<<"stop"; return; } } //END XLIFF markup handling else { if (charsRemoved) m_catalog->push(new DelTextCmd(m_catalog, pos, _oldMsgstr.mid(offset, charsRemoved))); _oldMsgstr = editText; //newStr becomes OldStr _oldMsgstrAscii = editTextAscii; //qCWarning(LOKALIZE_LOG)<<"char"<push(new InsTextCmd(m_catalog, pos, addedText.toString())); } /* TODO if (_leds) { if (m_catalog->msgstr(pos).isEmpty()) _leds->ledUntr->on(); else _leds->ledUntr->off(); } */ requestToggleApprovement(); reflectUntranslatedState(); // for mergecatalog (remove entry from index) // and for statusbar emit contentsModified(m_currentPos); if (charsAdded == 1) { int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, offset - 1); int len = (offset - sp); int wordCompletionLength = Settings::self()->wordCompletionLength(); if (wordCompletionLength >= 3 && len >= wordCompletionLength) doCompletion(offset + 1); else if (m_completionBox) m_completionBox->hide(); } else if (m_completionBox) m_completionBox->hide(); //qCWarning(LOKALIZE_LOG)<<"finish"; } bool TranslationUnitTextEdit::removeTargetSubstring(int delStart, int delLen, bool refresh) { if (Q_UNLIKELY(m_currentPos.entry == -1)) return false; if (!::removeTargetSubstring(m_catalog, m_currentPos, delStart, delLen)) return false; requestToggleApprovement(); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); } emit contentsModified(m_currentPos.entry); return true; } void TranslationUnitTextEdit::insertCatalogString(CatalogString catStr, int start, bool refresh) { QString REMOVEME = QStringLiteral("REMOVEME"); CatalogString sourceForReferencing = m_catalog->sourceWithTags(m_currentPos); const CatalogString target = m_catalog->targetWithTags(m_currentPos); QHash id2tagIndex; int i = sourceForReferencing.tags.size(); while (--i >= 0) id2tagIndex.insert(sourceForReferencing.tags.at(i).id, i); //remove markup that is already in target, to avoid duplicates if the string being inserted contains it as well foreach (const InlineTag& tag, target.tags) { if (id2tagIndex.contains(tag.id)) sourceForReferencing.tags[id2tagIndex.value(tag.id)].id = REMOVEME; } //iterating from the end is essential i = sourceForReferencing.tags.size(); while (--i >= 0) if (sourceForReferencing.tags.at(i).id == REMOVEME) sourceForReferencing.tags.removeAt(i); adaptCatalogString(catStr, sourceForReferencing); ::insertCatalogString(m_catalog, m_currentPos, catStr, start); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, catStr.string.size()); setTextCursor(cursor); } } const QString LOKALIZE_XLIFF_MIMETYPE = QStringLiteral("application/x-lokalize-xliff+xml"); QMimeData* TranslationUnitTextEdit::createMimeDataFromSelection() const { QMimeData *mimeData = new QMimeData; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); QMap tagPlaces; if (fillTagPlaces(tagPlaces, catalogString, start, end - start)) { //transform CatalogString //TODO substring method catalogString.string = catalogString.string.mid(start, end - start); QList::iterator it = catalogString.tags.begin(); while (it != catalogString.tags.end()) { if (!tagPlaces.contains(it->start)) it = catalogString.tags.erase(it); else { it->start -= start; it->end -= start; ++it; } } QByteArray a; QDataStream out(&a, QIODevice::WriteOnly); QVariant v; - qVariantSetValue(v, catalogString); + v.setValue(catalogString); out << v; mimeData->setData(LOKALIZE_XLIFF_MIMETYPE, a); } QString text = catalogString.string; text.remove(TAGRANGE_IMAGE_SYMBOL); mimeData->setText(text); return mimeData; } void TranslationUnitTextEdit::dragEnterEvent(QDragEnterEvent * event) { QObject* dragSource = event->source(); if (dragSource->objectName().compare("qt_scrollarea_viewport") == 0) dragSource = dragSource->parent(); //This is a deplacement within the Target area if (m_part == DocPosition::Target && this == dragSource) { QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); m_cursorSelectionEnd = end; m_cursorSelectionStart = start; } QTextEdit::dragEnterEvent(event); } void TranslationUnitTextEdit::dropEvent(QDropEvent * event) { //Ensure the cursor moves to the correct location if (m_part == DocPosition::Target) { setTextCursor(cursorForPosition(event->pos())); //This is a copy modifier, disable the selection flags if (event->keyboardModifiers() & Qt::ControlModifier) { m_cursorSelectionEnd = 0; m_cursorSelectionStart = 0; } } QTextEdit::dropEvent(event); } void TranslationUnitTextEdit::insertFromMimeData(const QMimeData * source) { if (m_part == DocPosition::Source) return; if (source->hasFormat(LOKALIZE_XLIFF_MIMETYPE)) { //qCWarning(LOKALIZE_LOG)<<"has"; QVariant v; QByteArray data = source->data(LOKALIZE_XLIFF_MIMETYPE); QDataStream in(&data, QIODevice::ReadOnly); in >> v; //qCWarning(LOKALIZE_LOG)<<"ins"<(v).string<(v).ranges.size(); int start = 0; m_catalog->beginMacro(i18nc("@item Undo action item", "Insert text with markup")); QTextCursor cursor = textCursor(); if (cursor.hasSelection()) { start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); removeTargetSubstring(start, end - start); cursor.setPosition(start); setTextCursor(cursor); } else //sets right cursor position implicitly -- needed for mouse paste { QMimeData mimeData; mimeData.setText(QString()); if (m_cursorSelectionEnd != m_cursorSelectionStart) { int oldCursorPosition = textCursor().position(); removeTargetSubstring(m_cursorSelectionStart, m_cursorSelectionEnd - m_cursorSelectionStart); if (oldCursorPosition >= m_cursorSelectionEnd) { cursor.setPosition(oldCursorPosition - (m_cursorSelectionEnd - m_cursorSelectionStart)); setTextCursor(cursor); } } KTextEdit::insertFromMimeData(&mimeData); start = textCursor().position(); } insertCatalogString(v.value(), start); m_catalog->endMacro(); } else { QString text = source->text(); text.remove(TAGRANGE_IMAGE_SYMBOL); insertPlainTextWithCursorCheck(text); } } static bool isMasked(const QString & str, uint col) { if (col == 0 || str.isEmpty()) return false; uint counter = 0; int pos = col; while (pos >= 0 && str.at(pos) == '\\') { counter++; pos--; } return !(bool)(counter % 2); } void TranslationUnitTextEdit::keyPressEvent(QKeyEvent * keyEvent) { QString spclChars = QStringLiteral("abfnrtv'?\\"); if (keyEvent->matches(QKeySequence::MoveToPreviousPage)) emit gotoPrevRequested(); else if (keyEvent->matches(QKeySequence::MoveToNextPage)) emit gotoNextRequested(); else if (keyEvent->matches(QKeySequence::Undo)) emit undoRequested(); else if (keyEvent->matches(QKeySequence::Redo)) emit redoRequested(); else if (keyEvent->matches(QKeySequence::Find)) emit findRequested(); else if (keyEvent->matches(QKeySequence::FindNext)) emit findNextRequested(); else if (keyEvent->matches(QKeySequence::Replace)) emit replaceRequested(); else if (keyEvent->modifiers() == (Qt::AltModifier | Qt::ControlModifier)) { if (keyEvent->key() == Qt::Key_Home) emit gotoFirstRequested(); else if (keyEvent->key() == Qt::Key_End) emit gotoLastRequested(); } else if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::MoveToPreviousLine)) { //static QTime lastUpDownPress; //if (lastUpDownPress.msecsTo(QTime::currentTime())<500) { keyEvent->setAccepted(true); bool up = keyEvent->key() == Qt::Key_Up; QTextCursor c = textCursor(); if (!c.movePosition(up ? QTextCursor::Up : QTextCursor::Down)) { QTextCursor::MoveOperation op; if (up && !c.atStart()) op = QTextCursor::Start; else if (!up && !c.atEnd()) op = QTextCursor::End; else if (up) { emit gotoPrevRequested(); op = QTextCursor::End; } else { emit gotoNextRequested(); op = QTextCursor::Start; } c.movePosition(op); } setTextCursor(c); } //lastUpDownPress=QTime::currentTime(); } else if (m_part == DocPosition::Source) return KTextEdit::keyPressEvent(keyEvent); //BEGIN GENERAL // ALT+123 feature TODO this is general so should be on another level else if ((keyEvent->modifiers()&Qt::AltModifier) && !keyEvent->text().isEmpty() && keyEvent->text().at(0).isDigit()) { QString text = keyEvent->text(); while (!text.isEmpty() && text.at(0).isDigit()) { m_currentUnicodeNumber = 10 * m_currentUnicodeNumber + (text.at(0).digitValue()); text.remove(0, 1); } KTextEdit::keyPressEvent(keyEvent); } //END GENERAL else if (!keyEvent->modifiers() && (keyEvent->key() == Qt::Key_Backspace || keyEvent->key() == Qt::Key_Delete)) { //only for cases when: //-BkSpace was hit and cursor was atStart //-Del was hit and cursor was atEnd if (Q_UNLIKELY(!m_catalog->isApproved(m_currentPos.entry) && !textCursor().hasSelection()) && ((textCursor().atStart() && keyEvent->key() == Qt::Key_Backspace) || (textCursor().atEnd() && keyEvent->key() == Qt::Key_Delete))) requestToggleApprovement(); else KTextEdit::keyPressEvent(keyEvent); } else if (keyEvent->key() == Qt::Key_Space && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainTextWithCursorCheck(QChar(0x00a0U)); else if (keyEvent->key() == Qt::Key_Minus && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainTextWithCursorCheck(QChar(0x0000AD)); //BEGIN clever editing else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { if (m_completionBox && m_completionBox->isVisible()) { if (m_completionBox->currentItem()) completionActivated(m_completionBox->currentItem()->text()); else qCWarning(LOKALIZE_LOG) << "avoided a crash. a case for bug 238835!"; m_completionBox->hide(); return; } if (m_catalog->type() != Gettext) return KTextEdit::keyPressEvent(keyEvent); QString str = toPlainText(); QTextCursor t = textCursor(); int pos = t.position(); QString ins; if (keyEvent->modifiers()&Qt::ShiftModifier) { if (pos > 0 && !str.isEmpty() && str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) { ins = 'n'; } else { ins = QStringLiteral("\\n"); } } else if (!(keyEvent->modifiers()&Qt::ControlModifier)) { if (m_langUsesSpaces && pos > 0 && !str.isEmpty() && !str.at(pos - 1).isSpace()) { if (str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) ins = QLatin1Char('\\'); // if there is no new line at the end if (pos < 2 || str.midRef(pos - 2, 2) != QLatin1String("\\n")) ins += QLatin1Char(' '); } else if (str.isEmpty()) { ins = QStringLiteral("\\n"); } } if (!str.isEmpty()) { ins += '\n'; insertPlainTextWithCursorCheck(ins); } else KTextEdit::keyPressEvent(keyEvent); } else if (m_catalog->type() != Gettext) KTextEdit::keyPressEvent(keyEvent); else if ((keyEvent->modifiers()&Qt::ControlModifier) ? (keyEvent->key() == Qt::Key_D) : (keyEvent->key() == Qt::Key_Delete) && textCursor().atEnd()) { qCWarning(LOKALIZE_LOG) << "workaround for Qt/X11 bug"; QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); //workaround for Qt/X11 bug: if Del on NumPad is pressed, then pos is beyond end if (pos == str.size()) --pos; if (!str.isEmpty() && str.at(pos) == '\\' && !isMasked(str, pos) && pos < str.length() - 1 && spclChars.contains(str.at(pos + 1))) { t.deleteChar(); } } t.deleteChar(); setTextCursor(t); } else if ((!keyEvent->modifiers() && keyEvent->key() == Qt::Key_Backspace) || ((keyEvent->modifiers() & Qt::ControlModifier) && keyEvent->key() == Qt::Key_H)) { QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); if (!str.isEmpty() && pos > 0 && spclChars.contains(str.at(pos - 1))) { if (pos > 1 && str.at(pos - 2) == QLatin1Char('\\') && !isMasked(str, pos - 2)) { t.deletePreviousChar(); t.deletePreviousChar(); setTextCursor(t); //qCWarning(LOKALIZE_LOG)<<"set-->"<key() == Qt::Key_Tab) insertPlainTextWithCursorCheck(QStringLiteral("\\t")); else KTextEdit::keyPressEvent(keyEvent); //END clever editing } void TranslationUnitTextEdit::keyReleaseEvent(QKeyEvent * e) { if ((e->key() == Qt::Key_Alt) && m_currentUnicodeNumber >= 32) { insertPlainTextWithCursorCheck(QChar(m_currentUnicodeNumber)); m_currentUnicodeNumber = 0; } else KTextEdit::keyReleaseEvent(e); } void TranslationUnitTextEdit::insertPlainTextWithCursorCheck(const QString & text) { insertPlainText(text); KTextEdit::ensureCursorVisible(); } QString TranslationUnitTextEdit::toPlainText() { QTextCursor cursor = textCursor(); cursor.select(QTextCursor::Document); QString text = cursor.selectedText(); text.replace(QChar(8233), '\n'); /* int ii=text.size(); while(--ii>=0) qCWarning(LOKALIZE_LOG)<push(new InsTagCmd(m_catalog, currentPos(), tag)); showPos(currentPos(), CatalogString(),/*keepCursor*/true); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, tag.end + 1 + tag.isPaired()); setFocus(); } int TranslationUnitTextEdit::strForMicePosIfUnderTag(QPoint mice, CatalogString & str, bool tryHarder) { if (m_currentPos.entry == -1) return -1; QTextCursor cursor = cursorForPosition(mice); int pos = cursor.position(); str = m_catalog->catalogString(m_currentPos); if (pos == -1 || pos >= str.string.size()) return -1; //qCWarning(LOKALIZE_LOG)<<"here1"<0) // { // cursor.movePosition(QTextCursor::Left); // mice.setX(mice.x()+cursorRect(cursor).width()/2); // pos=cursorForPosition(mice).position(); // } if (str.string.at(pos) != TAGRANGE_IMAGE_SYMBOL) { bool cont = tryHarder && --pos >= 0 && str.string.at(pos) == TAGRANGE_IMAGE_SYMBOL; if (!cont) return -1; } int result = str.tags.size(); while (--result >= 0 && str.tags.at(result).start != pos && str.tags.at(result).end != pos) ; return result; } void TranslationUnitTextEdit::mouseReleaseEvent(QMouseEvent * event) { if (event->button() == Qt::LeftButton) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1 && m_part == DocPosition::Source) { emit tagInsertRequested(str.tags.at(pos)); event->accept(); return; } } KTextEdit::mouseReleaseEvent(event); } void TranslationUnitTextEdit::contextMenuEvent(QContextMenuEvent * event) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1) { QString xid = str.tags.at(pos).xid; if (!xid.isEmpty()) { QMenu menu; int entry = m_catalog->unitById(xid); /* QAction* findUnit=menu.addAction(entry>=m_catalog->numberOfEntries()? i18nc("@action:inmenu","Show the binary unit"): i18nc("@action:inmenu","Go to the referenced entry")); */ QAction* result = menu.exec(event->globalPos()); if (result) { if (entry >= m_catalog->numberOfEntries()) emit binaryUnitSelectRequested(xid); else emit gotoEntryRequested(DocPosition(entry)); event->accept(); } return; } } if (textCursor().hasSelection() && m_part == DocPosition::Target) { QMenu menu; menu.addAction(i18nc("@action:inmenu", "Lookup selected text in translation memory")); if (menu.exec(event->globalPos())) emit tmLookupRequested(m_part, textCursor().selectedText()); return; } if (m_part != DocPosition::Source && m_part != DocPosition::Target) return; KTextEdit::contextMenuEvent(event); #if 0 QTextCursor wordSelectCursor = cursorForPosition(event->pos()); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) { QMenu menu; QMenu suggestions; foreach (const QString& s, m_highlighter->suggestionsForWord(wordSelectCursor.selectedText())) suggestions.addAction(s); if (!suggestions.isEmpty()) { QAction* answer = suggestions.exec(event->globalPos()); if (answer) { m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(answer->text()); m_catalog->endMacro(); } } } #endif // QMenu menu; // QAction* spellchecking=menu.addAction(); // event->accept(); } void TranslationUnitTextEdit::zoomRequestedSlot(qreal fontSize) { QFont curFont = font(); curFont.setPointSizeF(fontSize); setFont(curFont); } void TranslationUnitTextEdit::wheelEvent(QWheelEvent * event) { //Override default KTextEdit behavior which ignores Ctrl+wheelEvent when the field is not ReadOnly (i/o zooming) if (m_part == DocPosition::Target && !Settings::mouseWheelGo() && (event->modifiers() == Qt::ControlModifier)) { float delta = event->angleDelta().y() / 120.f; zoomInF(delta); //Also zoom in the source emit zoomRequested(font().pointSizeF()); return; } if (m_part == DocPosition::Source || !Settings::mouseWheelGo()) { if (event->modifiers() == Qt::ControlModifier) { float delta = event->angleDelta().y() / 120.f; zoomInF(delta); //Also zoom in the target emit zoomRequested(font().pointSizeF()); return; } return KTextEdit::wheelEvent(event); } switch (event->modifiers()) { case Qt::ControlModifier: if (event->angleDelta().y() > 0) emit gotoPrevFuzzyRequested(); else emit gotoNextFuzzyRequested(); break; case Qt::AltModifier: if (event->angleDelta().y() > 0) emit gotoPrevUntranslatedRequested(); else emit gotoNextUntranslatedRequested(); break; case Qt::ControlModifier + Qt::ShiftModifier: if (event->angleDelta().y() > 0) emit gotoPrevFuzzyUntrRequested(); else emit gotoNextFuzzyUntrRequested(); break; case Qt::ShiftModifier: return KTextEdit::wheelEvent(event); default: if (event->angleDelta().y() > 0) emit gotoPrevRequested(); else emit gotoNextRequested(); } } void TranslationUnitTextEdit::spellReplace() { QTextCursor wordSelectCursor = textCursor(); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (!m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) return; const QStringList& suggestions = m_highlighter->suggestionsForWord(wordSelectCursor.selectedText()); if (suggestions.isEmpty()) return; m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(suggestions.first()); m_catalog->endMacro(); } bool TranslationUnitTextEdit::event(QEvent * event) { #ifdef Q_OS_MAC if (event->type() == QEvent::InputMethod) { QInputMethodEvent* e = static_cast(event); insertPlainTextWithCursorCheck(e->commitString()); e->accept(); return true; } #endif if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); CatalogString str; int pos = strForMicePosIfUnderTag(helpEvent->pos(), str, true); if (pos != -1) { QString tooltip = str.tags.at(pos).displayName(); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } QString tip; QString langCode = m_highlighter->currentLanguage(); bool nospell = langCode.isEmpty(); if (nospell) langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale l(langCode); if (l.language() != QLocale::C) tip = l.nativeLanguageName() + QLatin1String(" ("); tip += langCode; if (l.language() != QLocale::C) tip += ')'; if (nospell) tip += QLatin1String(" - ") + i18n("no spellcheck available"); QToolTip::showText(helpEvent->globalPos(), tip); } return KTextEdit::event(event); } void TranslationUnitTextEdit::tagMenu() { doTag(false); } void TranslationUnitTextEdit::tagImmediate() { doTag(true); } void TranslationUnitTextEdit::doTag(bool immediate) { QMenu menu; QAction* txt = 0; CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); int count = sourceWithTags.tags.size(); if (count) { QMap tagIdToIndex = m_catalog->targetWithTags(m_currentPos).tagIdToIndex(); bool hasActive = false; for (int i = 0; i < count; ++i) { //txt=menu.addAction(sourceWithTags.ranges.at(i)); txt = menu.addAction(QString::number(i)/*+" "+sourceWithTags.ranges.at(i).id*/); txt->setData(QVariant(i)); if (!hasActive && !tagIdToIndex.contains(sourceWithTags.tags.at(i).id)) { if (immediate) { insertTag(sourceWithTags.tags.at(txt->data().toInt())); return; } hasActive = true; menu.setActiveAction(txt); } } if (immediate) return; txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (!txt) return; insertTag(sourceWithTags.tags.at(txt->data().toInt())); } else { if (Q_UNLIKELY(Project::instance()->markup().isEmpty())) return; //QRegExp tag("(<[^>]*>)+|\\&\\w+\\;"); QRegExp tag(Project::instance()->markup()); tag.setMinimal(true); QString en = m_catalog->sourceWithTags(m_currentPos).string; QString target(toPlainText()); en.remove('\n'); target.remove('\n'); int pos = 0; //tag.indexIn(en); int posInMsgStr = 0; while ((pos = tag.indexIn(en, pos)) != -1) { /* QString str(tag.cap(0)); str.replace("&","&&");*/ txt = menu.addAction(tag.cap(0)); pos += tag.matchedLength(); if (posInMsgStr != -1 && (posInMsgStr = target.indexOf(tag.cap(0), posInMsgStr)) == -1) { if (immediate) { insertPlainTextWithCursorCheck(txt->text()); return; } menu.setActiveAction(txt); } else if (posInMsgStr != -1) posInMsgStr += tag.matchedLength(); } if (!txt || immediate) return; //txt=menu.exec(_msgidEdit->mapToGlobal(QPoint(0,0))); txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (txt) insertPlainTextWithCursorCheck(txt->text()); } } void TranslationUnitTextEdit::source2target() { CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); QString text = sourceWithTags.string; QString out; QString ctxt = m_catalog->context(m_currentPos.entry).first(); QRegExp delimiter(QStringLiteral("\\s*,\\s*")); //TODO ask for the fillment if the first time. //BEGIN KDE specific part if (ctxt.startsWith(QLatin1String("NAME OF TRANSLATORS")) || text.startsWith(QLatin1String("_: NAME OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorLocalizedName())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorLocalizedName(); } } else if (ctxt.startsWith(QLatin1String("EMAIL OF TRANSLATORS")) || text.startsWith(QLatin1String("_: EMAIL OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorEmail())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorEmail(); } } else if (/*_catalog->isGeneratedFromDocbook() &&*/ text.startsWith(QLatin1String("ROLES_OF_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("\n" "\n" "
    ") + Settings::authorEmail() + QLatin1String("
    \n" "
    "); } else if (text.startsWith(QLatin1String("CREDIT_FOR_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("") + Settings::authorLocalizedName() + '\n' + QLatin1String("") + Settings::authorEmail() + QLatin1String(""); } //END KDE specific part else { m_catalog->beginMacro(i18nc("@item Undo action item", "Copy source to target")); removeTargetSubstring(0, -1,/*refresh*/false); insertCatalogString(sourceWithTags, 0,/*refresh*/false); m_catalog->endMacro(); showPos(m_currentPos, sourceWithTags,/*keepCursor*/false); requestToggleApprovement(); } if (!out.isEmpty()) { QTextCursor t = textCursor(); t.movePosition(QTextCursor::End); t.insertText(out); setTextCursor(t); } } void TranslationUnitTextEdit::requestToggleApprovement() { if (m_catalog->isApproved(m_currentPos.entry) || !Settings::autoApprove()) return; bool skip = m_catalog->isPlural(m_currentPos); if (skip) { skip = false; DocPos pos(m_currentPos); for (pos.form = 0; pos.form < m_catalog->numberOfPluralForms(); ++(pos.form)) skip = skip || !m_catalog->isModified(pos); } if (!skip) emit toggleApprovementRequested(); } void TranslationUnitTextEdit::cursorToStart() { QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); setTextCursor(t); } void TranslationUnitTextEdit::doCompletion(int pos) { - QElapsedTimer a; a.start(); QString target = m_catalog->targetWithTags(m_currentPos).string; int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, pos - 1); int len = (pos - sp) - 1; QStringList s = CompletionStorage::instance()->makeCompletion(QString::fromRawData(target.unicode() + sp + 1, len)); if (!m_completionBox) { //BEGIN creation m_completionBox = new MyCompletionBox(this); connect(m_completionBox, &MyCompletionBox::activated, this, &TranslationUnitTextEdit::completionActivated); m_completionBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); //END creation } m_completionBox->setItems(s); if (s.size() && !s.first().isEmpty()) { m_completionBox->setCurrentRow(0); //qApp->removeEventFilter( m_completionBox ); if (!m_completionBox->isVisible()) //NOTE remove the check if kdelibs gets adapted m_completionBox->show(); m_completionBox->resize(m_completionBox->sizeHint()); QPoint p = cursorRect().bottomRight(); if (p.x() < 10) //workaround Qt bug p.rx() += textCursor().verticalMovementX() + QFontMetrics(currentFont()).horizontalAdvance('W'); m_completionBox->move(viewport()->mapToGlobal(p)); } else m_completionBox->hide(); } void TranslationUnitTextEdit::doExplicitCompletion() { doCompletion(textCursor().anchor()); } void TranslationUnitTextEdit::completionActivated(const QString & semiWord) { QTextCursor cursor = textCursor(); cursor.insertText(semiWord); setTextCursor(cursor); }