diff --git a/src/project/project.cpp b/src/project/project.cpp index 43924f8..3de4790 100644 --- a/src/project/project.cpp +++ b/src/project/project.cpp @@ -1,503 +1,514 @@ /* **************************************************************************** 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(); 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(); m_sourceFilePathsReady = false; 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; } + +QString Project::relativePath(const QString& possiblyAbsPath) const +{ + if (QFileInfo(possiblyAbsPath).isAbsolute()) { + if (projectDir().endsWith('/')) + return QString(possiblyAbsPath).remove(projectDir()); + return QString(possiblyAbsPath).remove(projectDir() + QLatin1Char('/')); + } + return possiblyAbsPath; +} + 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) { qCWarning(LOKALIZE_LOG) << "Starting SourceFilesSearchJob on " << 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; Project::instance()->m_sourceFilePathsReady = true; 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_sourceFilePathsReady && 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/project.h b/src/project/project.h index ee882b0..4c686ad 100644 --- a/src/project/project.h +++ b/src/project/project.h @@ -1,219 +1,220 @@ /* **************************************************************************** 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 PROJECT_H #define PROJECT_H #include #include #include "projectbase.h" #define WEBQUERY_ENABLE class ProjectModel; class ProjectLocal; namespace GlossaryNS { class Glossary; } namespace GlossaryNS { class GlossaryWindow; } namespace TM { class TMManagerWin; } /** * Singleton object that represents project. * It is shared between EditorWindow 'mainwindows' that use the same project file. * Keeps project's KDirModel, Glossary and kross::actions * * GUI for config handling is implemented in prefs.cpp * * @short Singleton object that represents project */ ///////// * Also provides list of web-query scripts class Project: public ProjectBase { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.Project") //qdbuscpp2xml -m -s project.h -o org.kde.lokalize.Project.xml public: explicit Project(); virtual ~Project(); bool isLoaded()const { return !m_path.isEmpty(); } ProjectModel* model(); //void setPath(const QString& p){m_path=p;} QString path()const { return m_path; } QString projectDir()const { return m_projectDir; } QString poDir()const { return absolutePath(poBaseDir()); } QString potDir()const { return absolutePath(potBaseDir()); } QString branchDir()const { return absolutePath(ProjectBase::branchDir()); } QString glossaryPath()const { return absolutePath(glossaryTbx()); } QString qaPath()const { return absolutePath(mainQA()); } GlossaryNS::Glossary* glossary()const { return m_glossary; } QString altTransDir()const { return absolutePath(altDir()); } bool queryCloseForAuxiliaryWindows(); bool isFileMissing(const QString& filePath) const; void setDefaults() override; // private slots: // void initLater(); public slots: Q_SCRIPTABLE void load(const QString& newProjectPath, const QString& defaultTargetLangCode = QString(), const QString& defaultProjectId = QString()); Q_SCRIPTABLE void reinit(); Q_SCRIPTABLE void save(); Q_SCRIPTABLE QString translationsRoot()const { return poDir(); } Q_SCRIPTABLE QString templatesRoot()const { return potDir(); } Q_SCRIPTABLE QString targetLangCode()const { return ProjectBase::langCode(); } Q_SCRIPTABLE QString sourceLangCode()const { return ProjectBase::sourceLangCode(); } Q_SCRIPTABLE void init(const QString& path, const QString& kind, const QString& id, const QString& sourceLang, const QString& targetLang); Q_SCRIPTABLE QString kind()const { return ProjectBase::kind(); } Q_SCRIPTABLE QString absolutePath(const QString&) const; + Q_SCRIPTABLE QString relativePath(const QString&) const; Q_SCRIPTABLE void setDesirablePath(const QString& path) { m_desirablePath = path; } Q_SCRIPTABLE QString desirablePath() const { return m_desirablePath; } Q_SCRIPTABLE bool isTmSupported() const; signals: Q_SCRIPTABLE void loaded(); void fileOpenRequested(const QString&, const bool setAsActive); void closed(); public slots: void populateDirModel(); void populateGlossary(); void showTMManager(); GlossaryNS::GlossaryWindow* showGlossary(); GlossaryNS::GlossaryWindow* defineNewTerm(QString en = QString(), QString target = QString()); void projectOdfCreate(); private: static Project* _instance; static void cleanupProject(); public: static Project* instance(); static ProjectLocal* local() { return instance()->m_localConfig; } const QMultiMap& sourceFilePaths(); void resetSourceFilePaths() { m_sourceFilePaths.clear(); m_sourceFilePathsReady = false; } friend class FillSourceFilePathsJob; signals: void sourceFilePathsAreReady(); private: QString m_path; QString m_desirablePath; ProjectLocal* m_localConfig; ProjectModel* m_model; GlossaryNS::Glossary* m_glossary; GlossaryNS::GlossaryWindow* m_glossaryWindow; TM::TMManagerWin* m_tmManagerWindow; QMultiMap m_sourceFilePaths; bool m_sourceFilePathsReady; //cache QString m_projectDir; }; #endif diff --git a/src/project/projectbase.kcfg b/src/project/projectbase.kcfg index eb65137..30c13a5 100644 --- a/src/project/projectbase.kcfg +++ b/src/project/projectbase.kcfg @@ -1,95 +1,103 @@ kde-i18n-lists.h klocalizedstring.h i18n("default") kde en_US getMailingList() false Application ./ ../templates ./terms.tbx ./main.lqa & (<[^>]+>)+|(&[A-Za-z_:][A-Za-z0-9_\.:-]*;|%[0-9])+ 80 + + + + + + + + diff --git a/src/project/projectmodel.cpp b/src/project/projectmodel.cpp index 7773a4d..e616d13 100644 --- a/src/project/projectmodel.cpp +++ b/src/project/projectmodel.cpp @@ -1,1258 +1,1267 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2018 by Karl Ove Hufthammer Copyright (C) 2007-2015 by Nick Shaforostoff Copyright (C) 2009 by Viesturs Zarins Copyright (C) 2018-2019 by Simon Depiets Copyright (C) 2019 by Alexander Potashev 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 "projectmodel.h" #include #include #include #include #include #include #include #include #include #include "lokalize_debug.h" #include "project.h" #include "updatestatsjob.h" static int nodeCounter = 0; ProjectModel::ProjectModel(QObject *parent) : QAbstractItemModel(parent) , m_poModel(this) , m_potModel(this) , m_rootNode(NULL, -1, -1, -1) , m_dirIcon(QIcon::fromTheme(QStringLiteral("inode-directory"))) , m_poIcon(QIcon::fromTheme(QStringLiteral("flag-blue"))) , m_poInvalidIcon(QIcon::fromTheme(QStringLiteral("flag-red"))) , m_poComplIcon(QIcon::fromTheme(QStringLiteral("flag-green"))) , m_poEmptyIcon(QIcon::fromTheme(QStringLiteral("flag-yellow"))) , m_potIcon(QIcon::fromTheme(QStringLiteral("flag-black"))) , m_activeJob(NULL) , m_activeNode(NULL) , m_doneTimer(new QTimer(this)) , m_delayedReloadTimer(new QTimer(this)) , m_threadPool(new QThreadPool(this)) , m_completeScan(true) { m_threadPool->setMaxThreadCount(1); m_threadPool->setExpiryTimeout(-1); m_poModel.dirLister()->setAutoErrorHandlingEnabled(false, NULL); m_poModel.dirLister()->setNameFilter(QStringLiteral("*.po *.pot *.xlf *.xliff *.ts")); m_potModel.dirLister()->setAutoErrorHandlingEnabled(false, NULL); m_potModel.dirLister()->setNameFilter(QStringLiteral("*.pot")); connect(&m_poModel, &KDirModel::dataChanged, this, &ProjectModel::po_dataChanged); connect(&m_poModel, &KDirModel::rowsInserted, this, &ProjectModel::po_rowsInserted); connect(&m_poModel, &KDirModel::rowsRemoved, this, &ProjectModel::po_rowsRemoved); connect(&m_potModel, &KDirModel::dataChanged, this, &ProjectModel::pot_dataChanged); connect(&m_potModel, &KDirModel::rowsInserted, this, &ProjectModel::pot_rowsInserted); connect(&m_potModel, &KDirModel::rowsRemoved, this, &ProjectModel::pot_rowsRemoved); m_delayedReloadTimer->setSingleShot(true); m_doneTimer->setSingleShot(true); connect(m_doneTimer, &QTimer::timeout, this, &ProjectModel::updateTotalsChanged); connect(m_delayedReloadTimer, &QTimer::timeout, this, &ProjectModel::reload); setUrl(QUrl(), QUrl()); } ProjectModel::~ProjectModel() { m_dirsWaitingForMetadata.clear(); if (m_activeJob != NULL) m_activeJob->setStatus(-2); m_activeJob = NULL; for (int pos = 0; pos < m_rootNode.rows.count(); pos ++) deleteSubtree(m_rootNode.rows.at(pos)); } void ProjectModel::setUrl(const QUrl &poUrl, const QUrl &potUrl) { //qCDebug(LOKALIZE_LOG) << "ProjectModel::openUrl("<< poUrl.pathOrUrl() << +", " << potUrl.pathOrUrl() << ")"; emit loadingAboutToStart(); //cleanup old data m_dirsWaitingForMetadata.clear(); if (m_activeJob != NULL) m_activeJob->setStatus(-1); m_activeJob = NULL; if (m_rootNode.rows.count()) { beginRemoveRows(QModelIndex(), 0, m_rootNode.rows.count()); for (int pos = 0; pos < m_rootNode.rows.count(); pos ++) deleteSubtree(m_rootNode.rows.at(pos)); m_rootNode.rows.clear(); m_rootNode.poCount = 0; m_rootNode.resetMetaData(); endRemoveRows(); } //add trailing slashes to base URLs, needed for potToPo and poToPot m_poUrl = poUrl.adjusted(QUrl::StripTrailingSlash); m_potUrl = potUrl.adjusted(QUrl::StripTrailingSlash); if (!poUrl.isEmpty()) m_poModel.dirLister()->openUrl(m_poUrl, KDirLister::Reload); if (!potUrl.isEmpty()) m_potModel.dirLister()->openUrl(m_potUrl, KDirLister::Reload); } QUrl ProjectModel::beginEditing(const QModelIndex& index) { Q_ASSERT(index.isValid()); QModelIndex poIndex = poIndexForOuter(index); QModelIndex potIndex = potIndexForOuter(index); if (poIndex.isValid()) { KFileItem item = m_poModel.itemForIndex(poIndex); return item.url(); } else if (potIndex.isValid()) { //copy over the file QUrl potFile = m_potModel.itemForIndex(potIndex).url(); QUrl poFile = potToPo(potFile); //EditorTab::fileOpen takes care of this //be careful, copy only if file does not exist already. // if (!KIO::NetAccess::exists(poFile, KIO::NetAccess::DestinationSide, NULL)) // KIO::NetAccess::file_copy(potFile, poFile); return poFile; } else { Q_ASSERT(false); return QUrl(); } } void ProjectModel::reload() { setUrl(m_poUrl, m_potUrl); } //Theese methds update the combined model from POT and PO model changes. //Quite complex stuff here, better do not change anything. //TODO A comment from Viesturs Zarins 2009-05-17 20:53:11 UTC: //This is a design issue in projectview.cpp. The same issue happens when creating/deleting any folder in project. //When a node PO item is added, the existing POT node is deleted and new one created to represent both. //When view asks if there is more data in the new node, the POT model answers no, as all the data was already stored in POT node witch is now deleted. //To fix this either reuse the existing POT node or manually repopulate data form POT model. void ProjectModel::po_dataChanged(const QModelIndex& po_topLeft, const QModelIndex& po_bottomRight) { //nothing special here //map from source and propagate QModelIndex topLeft = indexForPoIndex(po_topLeft); QModelIndex bottomRight = indexForPoIndex(po_bottomRight); if (topLeft.row() == bottomRight.row() && itemForIndex(topLeft).isFile()) { //this code works fine only for lonely files //and fails for more complex changes //see bug 342959 emit dataChanged(topLeft, bottomRight); enqueueNodeForMetadataUpdate(nodeForIndex(topLeft.parent())); } else if (topLeft.row() == bottomRight.row() && itemForIndex(topLeft).isDir()) { //Something happened inside this folder, nothing to do on the folder itself } else if (topLeft.row() != bottomRight.row() && itemForIndex(topLeft).isDir() && itemForIndex(bottomRight).isDir()) { //Something happened between two folders, no need to reload them } else { qCWarning(LOKALIZE_LOG) << "Delayed reload triggered in po_dataChanged"; m_delayedReloadTimer->start(1000); } } void ProjectModel::pot_dataChanged(const QModelIndex& pot_topLeft, const QModelIndex& pot_bottomRight) { #if 0 //tricky here - some of the pot items may be represented by po items //let's propagate that all subitems changed QModelIndex pot_parent = pot_topLeft.parent(); QModelIndex parent = indexForPotIndex(pot_parent); ProjectNode* node = nodeForIndex(parent); int count = node->rows.count(); QModelIndex topLeft = index(0, pot_topLeft.column(), parent); QModelIndex bottomRight = index(count - 1, pot_bottomRight.column(), parent); emit dataChanged(topLeft, bottomRight); enqueueNodeForMetadataUpdate(nodeForIndex(topLeft.parent())); #else Q_UNUSED(pot_topLeft) Q_UNUSED(pot_bottomRight) qCWarning(LOKALIZE_LOG) << "Delayed reload triggered in pot_dataChanged"; m_delayedReloadTimer->start(1000); #endif } void ProjectModel::po_rowsInserted(const QModelIndex& po_parent, int first, int last) { QModelIndex parent = indexForPoIndex(po_parent); QModelIndex pot_parent = potIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); //insert po rows beginInsertRows(parent, first, last); for (int pos = first; pos <= last; pos ++) { ProjectNode * childNode = new ProjectNode(node, pos, pos, -1); node->rows.insert(pos, childNode); } node->poCount += last - first + 1; //update rowNumber for (int pos = last + 1; pos < node->rows.count(); pos++) node->rows[pos]->rowNumber = pos; endInsertRows(); //remove unneeded pot rows, update PO rows if (pot_parent.isValid() || !parent.isValid()) { QVector pot2PoMapping; generatePOTMapping(pot2PoMapping, po_parent, pot_parent); for (int pos = node->poCount; pos < node->rows.count(); pos ++) { ProjectNode* potNode = node->rows.at(pos); int potIndex = potNode->potRowNumber; int poIndex = pot2PoMapping[potIndex]; if (poIndex != -1) { //found pot node, that now has a PO index. //remove the pot node and change the corresponding PO node beginRemoveRows(parent, pos, pos); node->rows.remove(pos); deleteSubtree(potNode); endRemoveRows(); node->rows[poIndex]->potRowNumber = potIndex; //This change does not need notification //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); pos--; } } } enqueueNodeForMetadataUpdate(node); } void ProjectModel::pot_rowsInserted(const QModelIndex& pot_parent, int start, int end) { QModelIndex parent = indexForPotIndex(pot_parent); QModelIndex po_parent = poIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); int insertedCount = end + 1 - start; QVector newPotNodes; if (po_parent.isValid() || !parent.isValid()) { //this node containts mixed items - add and merge the stuff QVector pot2PoMapping; generatePOTMapping(pot2PoMapping, po_parent, pot_parent); //reassign affected PO row POT indices for (int pos = 0; pos < node->poCount; pos ++) { ProjectNode* n = node->rows[pos]; if (n->potRowNumber >= start) n->potRowNumber += insertedCount; } //assign new POT indices for (int potIndex = start; potIndex <= end; potIndex ++) { int poIndex = pot2PoMapping[potIndex]; if (poIndex != -1) { //found pot node, that has a PO index. //change the corresponding PO node node->rows[poIndex]->potRowNumber = potIndex; //This change does not need notification //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); } else newPotNodes.append(potIndex); } } else { for (int pos = start; pos < end; pos ++) newPotNodes.append(pos); } //insert standalone POT rows, preserving POT order int newNodesCount = newPotNodes.count(); if (newNodesCount) { int insertionPoint = node->poCount; while ((insertionPoint < node->rows.count()) && (node->rows[insertionPoint]->potRowNumber < start)) insertionPoint++; beginInsertRows(parent, insertionPoint, insertionPoint + newNodesCount - 1); for (int pos = 0; pos < newNodesCount; pos ++) { int potIndex = newPotNodes.at(pos); ProjectNode * childNode = new ProjectNode(node, insertionPoint, -1, potIndex); node->rows.insert(insertionPoint, childNode); insertionPoint++; } //renumber remaining POT rows for (int pos = insertionPoint; pos < node->rows.count(); pos ++) { node->rows[pos]->rowNumber = pos; node->rows[pos]->potRowNumber += insertedCount; } endInsertRows(); } enqueueNodeForMetadataUpdate(node); //FIXME if templates folder doesn't contain an equivalent of po folder then it's stats will be broken: // one way to fix this is to explicitly force scan of the files of the child folders of the 'node' } void ProjectModel::po_rowsRemoved(const QModelIndex& po_parent, int start, int end) { QModelIndex parent = indexForPoIndex(po_parent); //QModelIndex pot_parent = potIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); int removedCount = end + 1 - start; if ((!parent.isValid()) && (node->rows.count() == 0)) { qCDebug(LOKALIZE_LOG) << "po_rowsRemoved fail"; //events after removing entire contents return; } //remove PO rows QList potRowsToInsert; beginRemoveRows(parent, start, end); //renumber all rows after removed. for (int pos = end + 1; pos < node->rows.count(); pos ++) { ProjectNode* childNode = node->rows.at(pos); childNode->rowNumber -= removedCount; if (childNode->poRowNumber > end) node->rows[pos]->poRowNumber -= removedCount; } //remove for (int pos = end; pos >= start; pos --) { int potIndex = node->rows.at(pos)->potRowNumber; deleteSubtree(node->rows.at(pos)); node->rows.remove(pos); if (potIndex != -1) potRowsToInsert.append(potIndex); } node->poCount -= removedCount; endRemoveRows(); //< fires removed event - the list has to be consistent now //add back rows that have POT files and fix row order std::sort(potRowsToInsert.begin(), potRowsToInsert.end()); int insertionPoint = node->poCount; for (int pos = 0; pos < potRowsToInsert.count(); pos ++) { int potIndex = potRowsToInsert.at(pos); while (insertionPoint < node->rows.count() && node->rows[insertionPoint]->potRowNumber < potIndex) { node->rows[insertionPoint]->rowNumber = insertionPoint; insertionPoint ++; } beginInsertRows(parent, insertionPoint, insertionPoint); ProjectNode * childNode = new ProjectNode(node, insertionPoint, -1, potIndex); node->rows.insert(insertionPoint, childNode); insertionPoint++; endInsertRows(); } //renumber remaining rows while (insertionPoint < node->rows.count()) { node->rows[insertionPoint]->rowNumber = insertionPoint; insertionPoint++; } enqueueNodeForMetadataUpdate(node); } void ProjectModel::pot_rowsRemoved(const QModelIndex& pot_parent, int start, int end) { QModelIndex parent = indexForPotIndex(pot_parent); QModelIndex po_parent = poIndexForOuter(parent); ProjectNode * node = nodeForIndex(parent); int removedCount = end + 1 - start; if ((!parent.isValid()) && (node->rows.count() == 0)) { //events after removing entire contents return; } //First remove POT nodes int firstPOTToRemove = node->poCount; int lastPOTToRemove = node->rows.count() - 1; while (firstPOTToRemove <= lastPOTToRemove && node->rows[firstPOTToRemove]->potRowNumber < start) firstPOTToRemove ++; while (lastPOTToRemove >= firstPOTToRemove && node->rows[lastPOTToRemove]->potRowNumber > end) lastPOTToRemove --; if (firstPOTToRemove <= lastPOTToRemove) { beginRemoveRows(parent, firstPOTToRemove, lastPOTToRemove); for (int pos = lastPOTToRemove; pos >= firstPOTToRemove; pos --) { ProjectNode* childNode = node->rows.at(pos); Q_ASSERT(childNode->potRowNumber >= start); Q_ASSERT(childNode->potRowNumber <= end); deleteSubtree(childNode); node->rows.remove(pos); } //renumber remaining rows for (int pos = firstPOTToRemove; pos < node->rows.count(); pos ++) { node->rows[pos]->rowNumber = pos; node->rows[pos]->potRowNumber -= removedCount; } endRemoveRows(); } //now remove POT indices form PO rows if (po_parent.isValid() || !parent.isValid()) { for (int poIndex = 0; poIndex < node->poCount; poIndex ++) { ProjectNode * childNode = node->rows[poIndex]; int potIndex = childNode->potRowNumber; if (potIndex >= start && potIndex <= end) { //found PO node, that has a POT index in range. //change the corresponding PO node node->rows[poIndex]->potRowNumber = -1; //this change does not affect the model //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); } else if (childNode->potRowNumber > end) { //reassign POT indices childNode->potRowNumber -= removedCount; } } } enqueueNodeForMetadataUpdate(node); } int ProjectModel::columnCount(const QModelIndex& /*parent*/)const { return ProjectModelColumnCount; } QVariant ProjectModel::headerData(int section, Qt::Orientation, int role) const { const auto column = static_cast(section); switch (role) { case Qt::TextAlignmentRole: { switch (column) { // Align numeric columns to the right and other columns to the left // Qt::AlignAbsolute is needed for RTL languages, ref. https://phabricator.kde.org/D13098 case ProjectModelColumns::TotalCount: case ProjectModelColumns::TranslatedCount: case ProjectModelColumns::FuzzyCount: case ProjectModelColumns::UntranslatedCount: case ProjectModelColumns::IncompleteCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); default: return QVariant(Qt::AlignLeft); } } case Qt::DisplayRole: { switch (column) { case ProjectModelColumns::FileName: return i18nc("@title:column File name", "Name"); case ProjectModelColumns::Graph: return i18nc("@title:column Graphical representation of Translated/Fuzzy/Untranslated counts", "Graph"); case ProjectModelColumns::TotalCount: return i18nc("@title:column Number of entries", "Total"); case ProjectModelColumns::TranslatedCount: return i18nc("@title:column Number of entries", "Translated"); case ProjectModelColumns::FuzzyCount: return i18nc("@title:column Number of entries", "Not ready"); case ProjectModelColumns::UntranslatedCount: return i18nc("@title:column Number of entries", "Untranslated"); case ProjectModelColumns::IncompleteCount: return i18nc("@title:column Number of fuzzy or untranslated entries", "Incomplete"); case ProjectModelColumns::TranslationDate: return i18nc("@title:column", "Last Translation"); + case ProjectModelColumns::Comment: + return i18nc("@title:column", "Comment"); case ProjectModelColumns::SourceDate: return i18nc("@title:column", "Template Revision"); case ProjectModelColumns::LastTranslator: return i18nc("@title:column", "Last Translator"); default: return {}; } } default: return {}; } } Qt::ItemFlags ProjectModel::flags(const QModelIndex & index) const { if (static_cast(index.column()) == ProjectModelColumns::FileName) return Qt::ItemIsSelectable | Qt::ItemIsEnabled; else return Qt::ItemIsSelectable; } int ProjectModel::rowCount(const QModelIndex & parent /*= QModelIndex()*/) const { return nodeForIndex(parent)->rows.size(); } bool ProjectModel::hasChildren(const QModelIndex & parent /*= QModelIndex()*/) const { if (!parent.isValid()) return true; QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); return ((poIndex.isValid() && m_poModel.hasChildren(poIndex)) || (potIndex.isValid() && m_potModel.hasChildren(potIndex))); } bool ProjectModel::canFetchMore(const QModelIndex & parent) const { if (!parent.isValid()) return m_poModel.canFetchMore(QModelIndex()) || m_potModel.canFetchMore(QModelIndex()); QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); return ((poIndex.isValid() && m_poModel.canFetchMore(poIndex)) || (potIndex.isValid() && m_potModel.canFetchMore(potIndex))); } void ProjectModel::fetchMore(const QModelIndex & parent) { if (!parent.isValid()) { if (m_poModel.canFetchMore(QModelIndex())) m_poModel.fetchMore(QModelIndex()); if (m_potModel.canFetchMore(QModelIndex())) m_potModel.fetchMore(QModelIndex()); } else { QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); if (poIndex.isValid() && (m_poModel.canFetchMore(poIndex))) m_poModel.fetchMore(poIndex); if (potIndex.isValid() && (m_potModel.canFetchMore(potIndex))) m_potModel.fetchMore(potIndex); } } /** * we use QRect to pass data through QVariant tunnel * * order is tran, untr, fuzzy * left() top() width() * */ QVariant ProjectModel::data(const QModelIndex& index, const int role) const { if (!index.isValid()) return QVariant(); const auto column = static_cast(index.column()); const ProjectNode* node = nodeForIndex(index); const QModelIndex internalIndex = poOrPotIndexForOuter(index); if (!internalIndex.isValid()) return QVariant(); const KFileItem item = itemForIndex(index); const bool isDir = item.isDir(); const bool invalid_file = node->metaDataStatus == ProjectNode::Status::InvalidFile; const bool hasStats = node->metaDataStatus != ProjectNode::Status::NoStats; const int translated = node->translatedAsPerRole(); const int fuzzy = node->fuzzyAsPerRole(); const int untranslated = node->metaData.untranslated; + QString comment(QStringLiteral("")); + int existingItem = Project::instance()->commentsFiles().indexOf(Project::instance()->relativePath(item.localPath())); + if (existingItem != -1 && Project::instance()->commentsTexts().count() > existingItem) { + comment = Project::instance()->commentsTexts().at(existingItem); + } switch (role) { case Qt::TextAlignmentRole: return ProjectModel::headerData(index.column(), Qt::Horizontal, role); // Use same alignment as header case Qt::DisplayRole: switch (column) { case ProjectModelColumns::FileName: return item.text(); case ProjectModelColumns::Graph: return hasStats ? QRect(translated, untranslated, fuzzy, 0) : QVariant(); case ProjectModelColumns::TotalCount: return hasStats ? (translated + untranslated + fuzzy) : QVariant(); case ProjectModelColumns::TranslatedCount: return hasStats ? translated : QVariant(); case ProjectModelColumns::FuzzyCount: return hasStats ? fuzzy : QVariant(); case ProjectModelColumns::UntranslatedCount: return hasStats ? untranslated : QVariant(); case ProjectModelColumns::IncompleteCount: return hasStats ? (untranslated + fuzzy) : QVariant(); + case ProjectModelColumns::Comment: + return comment; case ProjectModelColumns::SourceDate: return node->metaData.sourceDate; case ProjectModelColumns::TranslationDate: return node->metaData.translationDate; case ProjectModelColumns::LastTranslator: return node->metaData.lastTranslator; default: return {}; } case Qt::ToolTipRole: if (column == ProjectModelColumns::FileName) { return item.text(); } else { return {}; } case KDirModel::FileItemRole: return QVariant::fromValue(item); case Qt::DecorationRole: if (column != ProjectModelColumns::FileName) { return QVariant(); } if (isDir) return m_dirIcon; if (invalid_file) return m_poInvalidIcon; else if (hasStats && fuzzy == 0 && untranslated == 0) { if (translated == 0) return m_poEmptyIcon; else return m_poComplIcon; } else if (node->poRowNumber != -1) return m_poIcon; else if (node->potRowNumber != -1) return m_potIcon; else return QVariant(); case FuzzyUntrCountAllRole: return hasStats ? (fuzzy + untranslated) : 0; case FuzzyUntrCountRole: return item.isFile() ? (fuzzy + untranslated) : 0; case FuzzyCountRole: return item.isFile() ? fuzzy : 0; case UntransCountRole: return item.isFile() ? untranslated : 0; case TemplateOnlyRole: return item.isFile() ? (node->poRowNumber == -1) : 0; case TransOnlyRole: return item.isFile() ? (node->potRowNumber == -1) : 0; case DirectoryRole: return isDir ? 1 : 0; case TotalRole: return hasStats ? (fuzzy + untranslated + translated) : 0; default: return QVariant(); } } QModelIndex ProjectModel::index(int row, int column, const QModelIndex& parent) const { ProjectNode* parentNode = nodeForIndex(parent); //qCWarning(LOKALIZE_LOG)<<(sizeof(ProjectNode))<= parentNode->rows.size()) { qCWarning(LOKALIZE_LOG) << "Issues with indexes" << row << parentNode->rows.size() << itemForIndex(parent).url(); return QModelIndex(); } return createIndex(row, column, parentNode->rows.at(row)); } KFileItem ProjectModel::itemForIndex(const QModelIndex& index) const { if (!index.isValid()) { //file item for root node. return m_poModel.itemForIndex(index); } QModelIndex poIndex = poIndexForOuter(index); if (poIndex.isValid()) return m_poModel.itemForIndex(poIndex); else { QModelIndex potIndex = potIndexForOuter(index); if (potIndex.isValid()) return m_potModel.itemForIndex(potIndex); } qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.row() << index.column(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().isValid(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().internalPointer(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().data().toString(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.internalPointer(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << static_cast(index.internalPointer())->metaData.untranslated << static_cast(index.internalPointer())->metaData.sourceDate; return KFileItem(); } ProjectModel::ProjectNode* ProjectModel::nodeForIndex(const QModelIndex& index) const { if (index.isValid()) { ProjectNode * node = static_cast(index.internalPointer()); Q_ASSERT(node != NULL); return node; } else { ProjectNode * node = const_cast(&m_rootNode); Q_ASSERT(node != NULL); return node; } } QModelIndex ProjectModel::indexForNode(const ProjectNode* node) { if (node == &m_rootNode) return QModelIndex(); int row = node->rowNumber; QModelIndex index = createIndex(row, 0, (void*)node); return index; } QModelIndex ProjectModel::indexForUrl(const QUrl& url) { if (m_poUrl.isParentOf(url)) { QModelIndex poIndex = m_poModel.indexForUrl(url); return indexForPoIndex(poIndex); } else if (m_potUrl.isParentOf(url)) { QModelIndex potIndex = m_potModel.indexForUrl(url); return indexForPotIndex(potIndex); } return QModelIndex(); } QModelIndex ProjectModel::parent(const QModelIndex& childIndex) const { if (!childIndex.isValid()) return QModelIndex(); ProjectNode* childNode = nodeForIndex(childIndex); ProjectNode* parentNode = childNode->parent; if (!parentNode || (childNode == &m_rootNode) || (parentNode == &m_rootNode)) return QModelIndex(); return createIndex(parentNode->rowNumber, 0, parentNode); } /** * Theese methods map from project model indices to PO and POT model indices. * In each folder files form PO model comes first, and files from POT that do not exist in PO model come after. */ QModelIndex ProjectModel::indexForOuter(const QModelIndex& outerIndex, IndexType type) const { if (!outerIndex.isValid()) return QModelIndex(); QModelIndex parent = outerIndex.parent(); QModelIndex internalParent; if (parent.isValid()) { internalParent = indexForOuter(parent, type); if (!internalParent.isValid()) return QModelIndex(); } ProjectNode* node = nodeForIndex(outerIndex); short rowNumber = (type == PoIndex ? node->poRowNumber : node->potRowNumber); if (rowNumber == -1) return QModelIndex(); return (type == PoIndex ? m_poModel : m_potModel).index(rowNumber, outerIndex.column(), internalParent); } QModelIndex ProjectModel::poIndexForOuter(const QModelIndex& outerIndex) const { return indexForOuter(outerIndex, PoIndex); } QModelIndex ProjectModel::potIndexForOuter(const QModelIndex& outerIndex) const { return indexForOuter(outerIndex, PotIndex); } QModelIndex ProjectModel::poOrPotIndexForOuter(const QModelIndex& outerIndex) const { if (!outerIndex.isValid()) return QModelIndex(); QModelIndex poIndex = poIndexForOuter(outerIndex); if (poIndex.isValid()) return poIndex; QModelIndex potIndex = potIndexForOuter(outerIndex); if (!potIndex.isValid()) qCWarning(LOKALIZE_LOG) << "error mapping index to PO or POT"; return potIndex; } QModelIndex ProjectModel::indexForPoIndex(const QModelIndex& poIndex) const { if (!poIndex.isValid()) return QModelIndex(); QModelIndex outerParent = indexForPoIndex(poIndex.parent()); int row = poIndex.row(); //keep the same row, no changes return index(row, poIndex.column(), outerParent); } QModelIndex ProjectModel::indexForPotIndex(const QModelIndex& potIndex) const { if (!potIndex.isValid()) return QModelIndex(); QModelIndex outerParent = indexForPotIndex(potIndex.parent()); ProjectNode* node = nodeForIndex(outerParent); int potRow = potIndex.row(); int row = 0; while (row < node->rows.count() && node->rows.at(row)->potRowNumber != potRow) row++; if (row != node->rows.count()) return index(row, potIndex.column(), outerParent); qCWarning(LOKALIZE_LOG) << "error mapping index from POT to outer, searched for potRow:" << potRow; return QModelIndex(); } /** * Makes a list of indices where pot items map to poItems. * result[potRow] = poRow or -1 if the pot entry is not found in po. * Does not use internal pot and po row number cache. */ void ProjectModel::generatePOTMapping(QVector & result, const QModelIndex& poParent, const QModelIndex& potParent) const { result.clear(); int poRows = m_poModel.rowCount(poParent); int potRows = m_potModel.rowCount(potParent); if (potRows == 0) return; QList poOccupiedUrls; for (int poPos = 0; poPos < poRows; poPos ++) { KFileItem file = m_poModel.itemForIndex(m_poModel.index(poPos, 0, poParent)); QUrl potUrl = poToPot(file.url()); poOccupiedUrls.append(potUrl); } for (int potPos = 0; potPos < potRows; potPos ++) { QUrl potUrl = m_potModel.itemForIndex(m_potModel.index(potPos, 0, potParent)).url(); int occupiedPos = -1; //TODO: this is slow for (int poPos = 0; occupiedPos == -1 && poPos < poOccupiedUrls.count(); poPos ++) { QUrl& occupiedUrl = poOccupiedUrls[poPos]; if (potUrl.matches(occupiedUrl, QUrl::StripTrailingSlash)) occupiedPos = poPos; } result.append(occupiedPos); } } QUrl ProjectModel::poToPot(const QUrl& poPath) const { if (!(m_poUrl.isParentOf(poPath) || m_poUrl.matches(poPath, QUrl::StripTrailingSlash))) { qCWarning(LOKALIZE_LOG) << "PO path not in project: " << poPath.url(); return QUrl(); } QString pathToAdd = QDir(m_poUrl.path()).relativeFilePath(poPath.path()); //change ".po" into ".pot" if (pathToAdd.endsWith(QLatin1String(".po"))) //TODO: what about folders ?? pathToAdd += 't'; QUrl potPath = m_potUrl; potPath.setPath(potPath.path() + '/' + pathToAdd); //qCDebug(LOKALIZE_LOG) << "ProjectModel::poToPot("<< poPath.pathOrUrl() << +") = " << potPath.pathOrUrl(); return potPath; } QUrl ProjectModel::potToPo(const QUrl& potPath) const { if (!(m_potUrl.isParentOf(potPath) || m_potUrl.matches(potPath, QUrl::StripTrailingSlash))) { qCWarning(LOKALIZE_LOG) << "POT path not in project: " << potPath.url(); return QUrl(); } QString pathToAdd = QDir(m_potUrl.path()).relativeFilePath(potPath.path()); //change ".pot" into ".po" if (pathToAdd.endsWith(QLatin1String(".pot"))) //TODO: what about folders ?? pathToAdd = pathToAdd.left(pathToAdd.length() - 1); QUrl poPath = m_poUrl; poPath.setPath(poPath.path() + '/' + pathToAdd); //qCDebug(LOKALIZE_LOG) << "ProjectModel::potToPo("<< potPath.pathOrUrl() << +") = " << poPath.pathOrUrl(); return poPath; } //Metadata stuff //For updating translation stats void ProjectModel::enqueueNodeForMetadataUpdate(ProjectNode* node) { //qCWarning(LOKALIZE_LOG) << "Enqueued node for metadata Update : " << node->rowNumber; m_doneTimer->stop(); if (m_dirsWaitingForMetadata.contains(node)) { if ((m_activeJob != NULL) && (m_activeNode == node)) m_activeJob->setStatus(-1); return; } m_dirsWaitingForMetadata.insert(node); if (m_activeJob == NULL) startNewMetadataJob(); } void ProjectModel::deleteSubtree(ProjectNode* node) { for (int row = 0; row < node->rows.count(); row ++) deleteSubtree(node->rows.at(row)); m_dirsWaitingForMetadata.remove(node); if ((m_activeJob != NULL) && (m_activeNode == node)) m_activeJob->setStatus(-1); delete node; } void ProjectModel::startNewMetadataJob() { if (!m_completeScan) //hack for debugging return; m_activeJob = NULL; m_activeNode = NULL; if (m_dirsWaitingForMetadata.isEmpty()) return; ProjectNode* node = *m_dirsWaitingForMetadata.constBegin(); //prepare new work m_activeNode = node; QList files; QModelIndex item = indexForNode(node); for (int row = 0; row < node->rows.count(); row ++) { KFileItem fileItem = itemForIndex(index(row, 0, item)); if (fileItem.isFile())//Do not seek items that are not files files.append(fileItem); } m_activeJob = new UpdateStatsJob(files, this); connect(m_activeJob, &UpdateStatsJob::done, this, &ProjectModel::finishMetadataUpdate); m_threadPool->start(m_activeJob); } void ProjectModel::finishMetadataUpdate(UpdateStatsJob* job) { if (job->m_status == -2) { delete job; return; } if ((m_dirsWaitingForMetadata.contains(m_activeNode)) && (job->m_status == 0)) { m_dirsWaitingForMetadata.remove(m_activeNode); //store the results setMetadataForDir(m_activeNode, m_activeJob->m_info); QModelIndex item = indexForNode(m_activeNode); //scan dubdirs - initiate data loading into the model. for (int row = 0; row < m_activeNode->rows.count(); row++) { QModelIndex child = index(row, 0, item); if (canFetchMore(child)) fetchMore(child); //QCoreApplication::processEvents(); } } delete m_activeJob; m_activeJob = 0; startNewMetadataJob(); } void ProjectModel::slotFileSaved(const QString& filePath) { QModelIndex index = indexForUrl(QUrl::fromLocalFile(filePath)); if (!index.isValid()) return; QList files; files.append(itemForIndex(index)); UpdateStatsJob* j = new UpdateStatsJob(files); connect(j, &UpdateStatsJob::done, this, &ProjectModel::finishSingleMetadataUpdate); m_threadPool->start(j); } void ProjectModel::finishSingleMetadataUpdate(UpdateStatsJob* job) { if (job->m_status != 0) { delete job; return; } const FileMetaData& info = job->m_info.first(); QModelIndex index = indexForUrl(QUrl::fromLocalFile(info.filePath)); if (!index.isValid()) return; ProjectNode* node = nodeForIndex(index); node->setFileStats(job->m_info.first()); updateDirStats(nodeForIndex(index.parent())); QModelIndex topLeft = index.sibling(index.row(), static_cast(ProjectModelColumns::Graph)); QModelIndex bottomRight = index.sibling(index.row(), ProjectModelColumnCount - 1); emit dataChanged(topLeft, bottomRight); delete job; } void ProjectModel::setMetadataForDir(ProjectNode* node, const QList& data) { const QModelIndex item = indexForNode(node); const int dataCount = data.count(); int rowsCount = 0; for (int row = 0; row < node->rows.count(); row++) if (itemForIndex(index(row, 0, item)).isFile()) rowsCount++; //Q_ASSERT(dataCount == rowsCount); if (dataCount != rowsCount) { m_delayedReloadTimer->start(2000); qCWarning(LOKALIZE_LOG) << "dataCount != rowsCount, scheduling full refresh"; return; } int dataId = 0; for (int row = 0; row < node->rows.count(); row++) { if (itemForIndex(index(row, 0, item)).isFile()) { node->rows[row]->setFileStats(data.at(dataId)); dataId++; } } if (!dataCount) return; updateDirStats(node); const QModelIndex topLeft = index(0, static_cast(ProjectModelColumns::Graph), item); const QModelIndex bottomRight = index(rowsCount - 1, ProjectModelColumnCount - 1, item); emit dataChanged(topLeft, bottomRight); } void ProjectModel::updateDirStats(ProjectNode* node) { node->calculateDirStats(); if (node == &m_rootNode) { updateTotalsChanged(); return; } updateDirStats(node->parent); if (node->parent->rows.count() == 0 || node->parent->rows.count() >= node->rowNumber) return; QModelIndex index = indexForNode(node); qCDebug(LOKALIZE_LOG) << index.row() << node->parent->rows.count(); if (index.row() >= node->parent->rows.count()) return; QModelIndex topLeft = index.sibling(index.row(), static_cast(ProjectModelColumns::Graph)); QModelIndex bottomRight = index.sibling(index.row(), ProjectModelColumnCount - 1); emit dataChanged(topLeft, bottomRight); } bool ProjectModel::updateDone(const QModelIndex& index, const KDirModel& model) { if (model.canFetchMore(index)) return false; int row = model.rowCount(index); while (--row >= 0) { if (!updateDone(model.index(row, 0, index), model)) return false; } return true; } void ProjectModel::updateTotalsChanged() { bool done = m_dirsWaitingForMetadata.isEmpty(); if (done) { done = updateDone(m_poModel.indexForUrl(m_poUrl), m_poModel) && updateDone(m_potModel.indexForUrl(m_potUrl), m_potModel); if (m_rootNode.fuzzyAsPerRole() + m_rootNode.translatedAsPerRole() + m_rootNode.metaData.untranslated > 0 && !done) m_doneTimer->start(2000); emit loadingFinished(); } emit totalsChanged(m_rootNode.fuzzyAsPerRole(), m_rootNode.translatedAsPerRole(), m_rootNode.metaData.untranslated, done); } //ProjectNode class ProjectModel::ProjectNode::ProjectNode(ProjectNode* _parent, int _rowNum, int _poIndex, int _potIndex) : parent(_parent) , rowNumber(_rowNum) , poRowNumber(_poIndex) , potRowNumber(_potIndex) , poCount(0) , metaDataStatus(Status::NoStats) , metaData() { ++nodeCounter; } ProjectModel::ProjectNode::~ProjectNode() { --nodeCounter; } void ProjectModel::ProjectNode::calculateDirStats() { metaData.fuzzy = 0; metaData.fuzzy_reviewer = 0; metaData.fuzzy_approver = 0; metaData.translated = 0; metaData.translated_reviewer = 0; metaData.translated_approver = 0; metaData.untranslated = 0; metaDataStatus = ProjectNode::Status::HasStats; for (int pos = 0; pos < rows.count(); pos++) { ProjectNode* child = rows.at(pos); if (child->metaDataStatus == ProjectNode::Status::HasStats) { metaData.fuzzy += child->metaData.fuzzy; metaData.fuzzy_reviewer += child->metaData.fuzzy_reviewer; metaData.fuzzy_approver += child->metaData.fuzzy_approver; metaData.translated += child->metaData.translated; metaData.translated_reviewer += child->metaData.translated_reviewer; metaData.translated_approver += child->metaData.translated_approver; metaData.untranslated += child->metaData.untranslated; } } } void ProjectModel::ProjectNode::setFileStats(const FileMetaData& info) { metaData = info; metaDataStatus = info.invalid_file ? Status::InvalidFile : Status::HasStats; } void ProjectModel::ProjectNode::resetMetaData() { metaDataStatus = Status::NoStats; metaData = FileMetaData(); } diff --git a/src/project/projectmodel.h b/src/project/projectmodel.h index e868d1d..3904d48 100644 --- a/src/project/projectmodel.h +++ b/src/project/projectmodel.h @@ -1,255 +1,256 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2018 by Karl Ove Hufthammer Copyright (C) 2007-2014 by Nick Shaforostoff Copyright (C) 2009 by Viesturs Zarins Copyright (C) 2018-2019 by Simon Depiets Copyright (C) 2019 by Alexander Potashev 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 PROJECTMODEL_H #define PROJECTMODEL_H #include #include #include #include "project.h" #include "projectlocal.h" #include "metadata/filemetadata.h" class QTimer; class QThreadPool; class UpdateStatsJob; /** * Some notes: * Uses two KDirModels for template and translations dir. * Listens to their signals and constructs a combined model. * Stats calculation: * Uses threadweawer for stats calculation. * Each job analyzes files in one dir and adds subdirs to queue. * A change in one file forces whole dir to be rescanned * The job priority needs some tweaking. */ class ProjectModel: public QAbstractItemModel { Q_OBJECT class ProjectNode { public: ProjectNode() = delete; explicit ProjectNode(const ProjectNode&) = delete; ProjectNode(ProjectNode* parent, int rowNum, int poIndex, int potIndex); ~ProjectNode(); void calculateDirStats(); void setFileStats(const FileMetaData& info); int translatedAsPerRole() const { switch (Project::local()->role()) { case ProjectLocal::Translator: case ProjectLocal::Undefined: return metaData.translated; case ProjectLocal::Reviewer: return metaData.translated_reviewer; case ProjectLocal::Approver: return metaData.translated_approver; } return -1; } int fuzzyAsPerRole() const { switch (Project::local()->role()) { case ProjectLocal::Translator: case ProjectLocal::Undefined: return metaData.fuzzy; case ProjectLocal::Reviewer: return metaData.fuzzy_reviewer; case ProjectLocal::Approver: return metaData.fuzzy_approver; } return -1; } void resetMetaData(); ProjectNode* parent; short rowNumber; //in parent's list short poRowNumber; //row number in po model, -1 if this has no po item. short potRowNumber; //row number in pot model, -1 if this has no pot item. short poCount; //number of items from PO in rows. The others will be form POT exclusively. QVector rows; //rows from po and pot, pot rows start from poCount; enum class Status { // metadata not initialized yet NoStats, // tried to initialize metadata, but failed InvalidFile, // metadata is initialized HasStats, }; Status metaDataStatus; FileMetaData metaData; }; public: enum class ProjectModelColumns { FileName = 0, Graph, TotalCount, TranslatedCount, FuzzyCount, UntranslatedCount, IncompleteCount, + Comment, SourceDate, TranslationDate, LastTranslator, ProjectModelColumnCount, }; const int ProjectModelColumnCount = static_cast(ProjectModelColumns::ProjectModelColumnCount); enum AdditionalRoles { FuzzyUntrCountRole = Qt::UserRole, FuzzyUntrCountAllRole, FuzzyCountRole, UntransCountRole, TemplateOnlyRole, TransOnlyRole, DirectoryRole, TotalRole }; explicit ProjectModel(QObject *parent); ~ProjectModel() override; void setUrl(const QUrl &poUrl, const QUrl &potUrl); QModelIndex indexForUrl(const QUrl& url); KFileItem itemForIndex(const QModelIndex& index) const; QUrl beginEditing(const QModelIndex& index); //copies POT file to PO file and returns url of the PO file // QAbstractItemModel methods int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex& index) const override; QVariant data(const QModelIndex& index, const int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; bool hasChildren(const QModelIndex& parent = QModelIndex()) const override; bool canFetchMore(const QModelIndex& parent) const override; void fetchMore(const QModelIndex& parent) override; QThreadPool* threadPool() { return m_threadPool; } void setCompleteScan(bool enable) { m_completeScan = enable; } signals: void totalsChanged(int fuzzy, int translated, int untranslated, bool done); void loadingAboutToStart(); void loadingFinished(); //may be emitted a bit earlier private slots: void po_dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void po_rowsInserted(const QModelIndex& parent, int start, int end); void po_rowsRemoved(const QModelIndex& parent, int start, int end); void pot_dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void pot_rowsInserted(const QModelIndex& parent, int start, int end); void pot_rowsRemoved(const QModelIndex& parent, int start, int end); void finishMetadataUpdate(UpdateStatsJob*); void finishSingleMetadataUpdate(UpdateStatsJob*); void updateTotalsChanged(); public slots: void slotFileSaved(const QString& filePath); void reload(); private: ProjectNode* nodeForIndex(const QModelIndex& index) const; QModelIndex indexForNode(const ProjectNode* node); enum IndexType {PoIndex, PotIndex}; QModelIndex indexForOuter(const QModelIndex& outerIndex, IndexType type) const; QModelIndex poIndexForOuter(const QModelIndex& outerIndex) const; QModelIndex potIndexForOuter(const QModelIndex& outerIndex) const; QModelIndex poOrPotIndexForOuter(const QModelIndex& outerIndex) const; QModelIndex indexForPoIndex(const QModelIndex& poIndex) const; QModelIndex indexForPotIndex(const QModelIndex& potIndex) const; void generatePOTMapping(QVector & result, const QModelIndex& poParent, const QModelIndex& potParent) const; QUrl poToPot(const QUrl& path) const; QUrl potToPo(const QUrl& path) const; void enqueueNodeForMetadataUpdate(ProjectNode* node); void deleteSubtree(ProjectNode* node); void startNewMetadataJob(); void setMetadataForDir(ProjectNode* node, const QList& data); void updateDirStats(ProjectNode* node); bool updateDone(const QModelIndex& index, const KDirModel& model); QUrl m_poUrl; QUrl m_potUrl; KDirModel m_poModel; KDirModel m_potModel; ProjectNode m_rootNode; QVariant m_dirIcon; QVariant m_poIcon; QVariant m_poInvalidIcon; QVariant m_poComplIcon; QVariant m_poEmptyIcon; QVariant m_potIcon; //for updating stats QSet m_dirsWaitingForMetadata; UpdateStatsJob* m_activeJob; ProjectNode* m_activeNode; QTimer* m_doneTimer; QTimer* m_delayedReloadTimer; QThreadPool* m_threadPool; bool m_completeScan; }; #endif diff --git a/src/project/projecttab.cpp b/src/project/projecttab.cpp index 4a61bbd..4c0c40a 100644 --- a/src/project/projecttab.cpp +++ b/src/project/projecttab.cpp @@ -1,490 +1,526 @@ /* **************************************************************************** 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 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); + menu->addAction(i18nc("@action:inmenu", "Add a comment"), this, &ProjectTab::addComment); 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::addComment() +{ + QStringList files = m_browser->selectedItems(); + int i = files.size(); + QStringList previousCommentsTexts = Project::instance()->commentsTexts(); + QStringList previousCommentsFiles = Project::instance()->commentsFiles(); + QString previousComment(QStringLiteral("")); + if (i >= 1) { + //Retrieve previous comment (first one) + int existingItem = previousCommentsFiles.indexOf(Project::instance()->relativePath(files.at(0))); + if (existingItem != -1 && previousCommentsTexts.count() > existingItem) { + previousComment = previousCommentsTexts.at(existingItem); + } + } + + bool ok; + QString newComment = QInputDialog::getText(this, i18n("Project file comment"), i18n("Input a comment for this project file:"), QLineEdit::Normal, previousComment, &ok); + if (!ok) + return; + + while (--i >= 0) { + QString filePath = Project::instance()->relativePath(files.at(i)); + int existingItem = previousCommentsFiles.indexOf(filePath); + if (existingItem != -1 && previousCommentsTexts.count() > existingItem) { + previousCommentsTexts[existingItem] = newComment; + } else { + previousCommentsTexts << newComment; + previousCommentsFiles << filePath; + } + } + Project::instance()->setCommentsTexts(previousCommentsTexts); + Project::instance()->setCommentsFiles(previousCommentsFiles); + Project::instance()->save(); +} + 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/project/projecttab.h b/src/project/projecttab.h index 1669296..d484ff4 100644 --- a/src/project/projecttab.h +++ b/src/project/projecttab.h @@ -1,133 +1,134 @@ /* **************************************************************************** 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 PROJECTTAB_H #define PROJECTTAB_H #include "lokalizesubwindowbase.h" #include #include #include class QStackedLayout; class ProjectWidget; class QLineEdit; class QContextMenuEvent; class QProgressBar; /** * Project Overview Tab */ class ProjectTab: public LokalizeSubwindowBase2 { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.ProjectOverview") //qdbuscpp2xml -m -s projecttab.h -o org.kde.lokalize.ProjectOverview.xml public: explicit ProjectTab(QWidget *parent); ~ProjectTab() override = default; void contextMenuEvent(QContextMenuEvent *event) override; void hideDocks() override {} void showDocks() override {} KXMLGUIClient* guiClient() override { return (KXMLGUIClient*)this; } QString currentFilePath() override; int unitsCount() { return m_currentUnitsCount; } void setLegacyUnitsCount(int to); signals: void projectOpenRequested(QString path); void projectOpenRequested(); void fileOpenRequested(const QString&, const bool setAsActive); void searchRequested(const QStringList&); void replaceRequested(const QStringList&); void spellcheckRequested(const QStringList&); public slots: Q_SCRIPTABLE void setCurrentItem(const QString& url); Q_SCRIPTABLE QString currentItem() const; ///@returns list of selected files recursively Q_SCRIPTABLE QStringList selectedItems() const; Q_SCRIPTABLE bool currentItemIsTranslationFile() const; void showRealProjectOverview(); void showWelcomeScreen(); //Q_SCRIPTABLE bool isShown() const; private slots: void setFilterRegExp(); void setFocus(); void scanFilesToTM(); void pologyOnFiles(); + void addComment(); void searchInFiles(bool templ = false); void searchInFilesInclTempl(); void openFile(); void findInFiles(); void replaceInFiles(); void spellcheckFiles(); void gotoPrevFuzzyUntr(); void gotoNextFuzzyUntr(); void gotoPrevFuzzy(); void gotoNextFuzzy(); void gotoPrevUntranslated(); void gotoNextUntranslated(); void gotoPrevTemplateOnly(); void gotoNextTemplateOnly(); void gotoPrevTransOnly(); void gotoNextTransOnly(); void toggleTranslatedFiles(); void updateStatusBar(int fuzzy = 0, int translated = 0, int untranslated = 0, bool done = false); void initStatusBarProgress(); void pologyHasFinished(int exitCode, QProcess::ExitStatus exitStatus); private: ProjectWidget* m_browser; QLineEdit* m_filterEdit; QProgressBar* m_progressBar; QStackedLayout *m_stackedLayout; KProcess* m_pologyProcess; bool m_pologyProcessInProgress; int m_legacyUnitsCount, m_currentUnitsCount; }; #endif diff --git a/src/project/projectwidget.cpp b/src/project/projectwidget.cpp index 40b4f53..60ce200 100644 --- a/src/project/projectwidget.cpp +++ b/src/project/projectwidget.cpp @@ -1,558 +1,559 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-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 "projectwidget.h" #include "lokalize_debug.h" #include "project.h" #include "catalog.h" #include "headerviewmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class PoItemDelegate: public QStyledItemDelegate { public: PoItemDelegate(QObject *parent = 0); ~PoItemDelegate() {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QString displayText(const QVariant & value, const QLocale & locale) const override; QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; private: KColorScheme m_colorScheme; }; PoItemDelegate::PoItemDelegate(QObject *parent) : QStyledItemDelegate(parent) , m_colorScheme(QPalette::Normal) {} QSize PoItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QString text = index.data().toString(); int lineCount = 1; int nPos = text.indexOf('\n'); if (nPos == -1) nPos = text.size(); else lineCount += text.count('\n'); static QFontMetrics metrics(option.font); return QSize(metrics.averageCharWidth() * nPos, metrics.height() * lineCount); } void PoItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (static_cast(index.column()) != ProjectModel::ProjectModelColumns::Graph) return QStyledItemDelegate::paint(painter, option, index); QVariant graphData = index.data(Qt::DisplayRole); if (Q_UNLIKELY(!graphData.isValid())) { painter->fillRect(option.rect, Qt::transparent); return; } QRect rect = graphData.toRect(); int translated = rect.left(); int untranslated = rect.top(); int fuzzy = rect.width(); int total = translated + untranslated + fuzzy; if (total > 0) { QBrush brush; painter->setPen(Qt::white); QRect myRect(option.rect); myRect.setWidth(option.rect.width() * translated / total); if (translated) { brush = m_colorScheme.foreground(KColorScheme::PositiveText); painter->fillRect(myRect, brush); } myRect.setLeft(myRect.left() + myRect.width()); myRect.setWidth(option.rect.width() * fuzzy / total); if (fuzzy) { brush = m_colorScheme.foreground(KColorScheme::NeutralText); painter->fillRect(myRect, brush); // painter->drawText(myRect,Qt::AlignRight,QString("%1").arg(data.width())); } myRect.setLeft(myRect.left() + myRect.width()); myRect.setWidth(option.rect.width() - myRect.left() + option.rect.left()); if (untranslated) brush = m_colorScheme.foreground(KColorScheme::NegativeText); //esle: paint what is left with the last brush used - blank, positive or neutral painter->fillRect(myRect, brush); // painter->drawText(myRect,Qt::AlignRight,QString("%1").arg(data.top())); } else if (total == -1) painter->fillRect(option.rect, Qt::transparent); else if (total == 0) painter->fillRect(option.rect, QBrush(Qt::gray)); } // Temporary workaround for Qt bug https://bugreports.qt.io/browse/QTBUG-78094 // to ensure that large numbers are formatted using a thousands separator QString PoItemDelegate::displayText(const QVariant & value, const QLocale & locale) const { return QStyledItemDelegate::displayText(value, QLocale::system()); } class SortFilterProxyModel : public KDirSortFilterProxyModel { public: SortFilterProxyModel(QObject* parent = nullptr) : KDirSortFilterProxyModel(parent) { connect(Project::instance()->model(), &ProjectModel::totalsChanged, this, &SortFilterProxyModel::invalidate); } ~SortFilterProxyModel() {} void toggleTranslatedFiles(); bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: bool m_hideTranslatedFiles = false; }; void SortFilterProxyModel::toggleTranslatedFiles() { m_hideTranslatedFiles = !m_hideTranslatedFiles; invalidateFilter(); } bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { bool result = false; const QAbstractItemModel* model = sourceModel(); QModelIndex item = model->index(source_row, 0, source_parent); /* if (model->hasChildren(item)) model->fetchMore(item); */ if (item.data(ProjectModel::DirectoryRole) == 1 && item.data(ProjectModel::TotalRole) == 0) return false; // Hide rows with no translations if they are folders if (item.data(ProjectModel::FuzzyUntrCountAllRole) == 0 && m_hideTranslatedFiles) return false; // Hide rows with no untranslated items if the filter is enabled int i = model->rowCount(item); while (--i >= 0 && !result) result = filterAcceptsRow(i, item); return result || QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } bool SortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { static QCollator collator; // qCWarning(LOKALIZE_LOG)<(sourceModel()); const KFileItem leftFileItem = projectModel->itemForIndex(left); const KFileItem rightFileItem = projectModel->itemForIndex(right); //Code taken from KDirSortFilterProxyModel, as it is not compatible with our model. //TODO: make KDirSortFilterProxyModel::subSortLessThan not cast model to KDirModel, but use data() with FileItemRole instead. // Directories and hidden files should always be on the top, independent // from the sort order. const bool isLessThan = (sortOrder() == Qt::AscendingOrder); if (leftFileItem.isNull() || rightFileItem.isNull()) { qCWarning(LOKALIZE_LOG) << ".isNull()"; return false; } // On our priority, folders go above regular files. if (leftFileItem.isDir() && !rightFileItem.isDir()) { return isLessThan; } else if (!leftFileItem.isDir() && rightFileItem.isDir()) { return !isLessThan; } // Hidden elements go before visible ones, if they both are // folders or files. if (leftFileItem.isHidden() && !rightFileItem.isHidden()) { return isLessThan; } else if (!leftFileItem.isHidden() && rightFileItem.isHidden()) { return !isLessThan; } // Hidden elements go before visible ones, if they both are // folders or files. if (leftFileItem.isHidden() && !rightFileItem.isHidden()) { return true; } else if (!leftFileItem.isHidden() && rightFileItem.isHidden()) { return false; } switch (static_cast(left.column())) { case ProjectModel::ProjectModelColumns::FileName: return collator.compare(leftFileItem.name(), rightFileItem.name()) < 0; case ProjectModel::ProjectModelColumns::Graph: { QRect leftRect(left.data(Qt::DisplayRole).toRect()); QRect rightRect(right.data(Qt::DisplayRole).toRect()); int leftAll = leftRect.left() + leftRect.top() + leftRect.width(); int rightAll = rightRect.left() + rightRect.top() + rightRect.width(); if (!leftAll || !rightAll) return false; float leftVal = (float)leftRect.left() / leftAll; float rightVal = (float)rightRect.left() / rightAll; if (leftVal < rightVal) return true; if (leftVal > rightVal) return false; leftVal = (float)leftRect.top() / leftAll; rightVal = (float)rightRect.top() / rightAll; if (leftVal < rightVal) return true; if (leftVal > rightVal) return false; leftVal = (float)leftRect.width() / leftAll; rightVal = (float)rightRect.width() / rightAll; if (leftVal < rightVal) return true; return false; } case ProjectModel::ProjectModelColumns::LastTranslator: case ProjectModel::ProjectModelColumns::SourceDate: case ProjectModel::ProjectModelColumns::TranslationDate: + case ProjectModel::ProjectModelColumns::Comment: return collator.compare(projectModel->data(left).toString(), projectModel->data(right).toString()) < 0; case ProjectModel::ProjectModelColumns::TotalCount: case ProjectModel::ProjectModelColumns::TranslatedCount: case ProjectModel::ProjectModelColumns::UntranslatedCount: case ProjectModel::ProjectModelColumns::IncompleteCount: case ProjectModel::ProjectModelColumns::FuzzyCount: return projectModel->data(left).toInt() < projectModel->data(right).toInt(); default: return false; } } ProjectWidget::ProjectWidget(/*Catalog* catalog, */QWidget* parent) : QTreeView(parent) , m_proxyModel(new SortFilterProxyModel(this)) // , m_catalog(catalog) { PoItemDelegate* delegate = new PoItemDelegate(this); setItemDelegate(delegate); connect(this, &ProjectWidget::activated, this, &ProjectWidget::slotItemActivated); m_proxyModel->setSourceModel(Project::instance()->model()); //m_proxyModel->setDynamicSortFilter(true); setModel(m_proxyModel); connect(Project::instance()->model(), &ProjectModel::loadingAboutToStart, this, &ProjectWidget::modelAboutToReload); connect(Project::instance()->model(), &ProjectModel::loadingFinished, this, &ProjectWidget::modelReloaded, Qt::QueuedConnection); setUniformRowHeights(true); setAllColumnsShowFocus(true); - int widthDefaults[] = {6, 1, 1, 1, 1, 1, 1, 4, 4, 4}; - //FileName, Graph, TotalCount, TranslatedCount, FuzzyCount, UntranslatedCount, IncompleteCount, SourceDate, TranslationDate, LastTranslator + int widthDefaults[] = {6, 1, 1, 1, 1, 1, 1, 4, 4, 4, 4}; + //FileName, Graph, TotalCount, TranslatedCount, FuzzyCount, UntranslatedCount, IncompleteCount, Comment, SourceDate, TranslationDate, LastTranslator int i = sizeof(widthDefaults) / sizeof(int); int baseWidth = columnWidth(0); while (--i >= 0) setColumnWidth(i, baseWidth * widthDefaults[i] / 2); setSortingEnabled(true); sortByColumn(0, Qt::AscendingOrder); setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows); // QTimer::singleShot(0,this,SLOT(initLater())); new HeaderViewMenuHandler(header()); KConfig config; KConfigGroup stateGroup(&config, "ProjectWindow"); header()->restoreState(QByteArray::fromBase64(stateGroup.readEntry("ListHeaderState", QByteArray()))); i = sizeof(widthDefaults) / sizeof(int); while (--i >= 0) { if (columnWidth(i) > 5 * baseWidth * widthDefaults[i]) { //The column width is more than 5 times its normal width setColumnWidth(i, 5 * baseWidth * widthDefaults[i]); } } } ProjectWidget::~ProjectWidget() { KConfig config; KConfigGroup stateGroup(&config, "ProjectWindow"); stateGroup.writeEntry("ListHeaderState", header()->saveState().toBase64()); } void ProjectWidget::modelAboutToReload() { m_currentItemPathBeforeReload = currentItem(); } void ProjectWidget::modelReloaded() { int i = 10; while (--i >= 0) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers | QEventLoop::WaitForMoreEvents, 100); if (setCurrentItem(m_currentItemPathBeforeReload)) break; } if (proxyModel()->filterRegExp().pattern().size() > 2) expandItems(); } bool ProjectWidget::setCurrentItem(const QString& u) { if (u.isEmpty()) return true; QModelIndex index = m_proxyModel->mapFromSource(Project::instance()->model()->indexForUrl(QUrl::fromLocalFile(u))); if (index.isValid()) setCurrentIndex(index); return index.isValid(); } QString ProjectWidget::currentItem() const { if (!currentIndex().isValid()) return QString(); return Project::instance()->model()->itemForIndex( m_proxyModel->mapToSource(currentIndex()) ).localPath(); } bool ProjectWidget::currentIsTranslationFile() const { //remember 'bout empty state return Catalog::extIsSupported(currentItem()); } void ProjectWidget::slotItemActivated(const QModelIndex& index) { if (currentIsTranslationFile()) { ProjectModel * srcModel = static_cast(static_cast(m_proxyModel)->sourceModel()); QModelIndex srcIndex = static_cast(m_proxyModel)->mapToSource(index); QUrl fileUrl = srcModel->beginEditing(srcIndex); emit fileOpenRequested(fileUrl.toLocalFile(), !(QApplication::keyboardModifiers() & Qt::ControlModifier)); } } void ProjectWidget::recursiveAdd(QStringList& list, const QModelIndex& idx) const { if (!m_proxyModel->filterAcceptsRow(idx.row(), idx.parent())) { return; } ProjectModel& model = *(Project::instance()->model()); const KFileItem& item(model.itemForIndex(idx)); if (item.isDir()) { int j = model.rowCount(idx); while (--j >= 0) { const KFileItem& childItem(model.itemForIndex(model.index(j, 0, idx))); if (childItem.isDir()) recursiveAdd(list, model.index(j, 0, idx)); else if (m_proxyModel->filterAcceptsRow(j, idx)) list.prepend(childItem.localPath()); } } else //if (!list.contains(u)) list.prepend(item.localPath()); } QStringList ProjectWidget::selectedItems() const { QStringList list; foreach (const QModelIndex& item, selectedIndexes()) { if (item.column() == 0) recursiveAdd(list, m_proxyModel->mapToSource(item)); } return list; } void ProjectWidget::expandItems(const QModelIndex& parent) { const QAbstractItemModel* m = model(); expand(parent); int i = m->rowCount(parent); while (--i >= 0) expandItems(m->index(i, 0, parent)); } bool ProjectWidget::gotoIndexCheck(const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role) { // Check if role is found for this index if (currentIndex.isValid()) { ProjectModel *srcModel = static_cast(static_cast(m_proxyModel)->sourceModel()); QModelIndex srcIndex = static_cast(m_proxyModel)->mapToSource(currentIndex); QVariant result = srcModel->data(srcIndex, role); return result.isValid() && result.toInt() > 0; } return false; } QModelIndex ProjectWidget::gotoIndexPrevNext(const QModelIndex& currentIndex, int direction) const { QModelIndex index = currentIndex; QModelIndex sibling; // Unless first or last sibling reached, continue with previous or next // sibling, otherwise continue with previous or next parent while (index.isValid()) { sibling = index.sibling(index.row() + direction, index.column()); if (sibling.isValid()) return sibling; index = index.parent(); } return index; } ProjectWidget::gotoIndexResult ProjectWidget::gotoIndexFind( const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction) { QModelIndex index = currentIndex; while (index.isValid()) { // Set current index and show it if role is found for this index if (gotoIndexCheck(index, role)) { clearSelection(); setCurrentIndex(index); scrollTo(index); return gotoIndex_found; } // Handle child recursively if index is not a leaf QModelIndex child = index.model()->index((direction == 1) ? 0 : (m_proxyModel->rowCount(index) - 1), index.column(), index); if (child.isValid()) { ProjectWidget::gotoIndexResult result = gotoIndexFind(child, role, direction); if (result != gotoIndex_notfound) return result; } // Go to previous or next item index = gotoIndexPrevNext(index, direction); } if (index.parent().isValid()) return gotoIndex_notfound; else return gotoIndex_end; } ProjectWidget::gotoIndexResult ProjectWidget::gotoIndex( const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction) { QModelIndex index = currentIndex; // Check if current index already found, and if so go to previous or next item if (gotoIndexCheck(index, role)) index = gotoIndexPrevNext(index, direction); return gotoIndexFind(index, role, direction); } void ProjectWidget::gotoPrevFuzzyUntr() { gotoIndex(currentIndex(), ProjectModel::FuzzyUntrCountRole, -1); } void ProjectWidget::gotoNextFuzzyUntr() { gotoIndex(currentIndex(), ProjectModel::FuzzyUntrCountRole, +1); } void ProjectWidget::gotoPrevFuzzy() { gotoIndex(currentIndex(), ProjectModel::FuzzyCountRole, -1); } void ProjectWidget::gotoNextFuzzy() { gotoIndex(currentIndex(), ProjectModel::FuzzyCountRole, +1); } void ProjectWidget::gotoPrevUntranslated() { gotoIndex(currentIndex(), ProjectModel::UntransCountRole, -1); } void ProjectWidget::gotoNextUntranslated() { gotoIndex(currentIndex(), ProjectModel::UntransCountRole, +1); } void ProjectWidget::gotoPrevTemplateOnly() { gotoIndex(currentIndex(), ProjectModel::TemplateOnlyRole, -1); } void ProjectWidget::gotoNextTemplateOnly() { gotoIndex(currentIndex(), ProjectModel::TemplateOnlyRole, +1); } void ProjectWidget::gotoPrevTransOnly() { gotoIndex(currentIndex(), ProjectModel::TransOnlyRole, -1); } void ProjectWidget::gotoNextTransOnly() { gotoIndex(currentIndex(), ProjectModel::TransOnlyRole, +1); } void ProjectWidget::toggleTranslatedFiles() { m_proxyModel->toggleTranslatedFiles(); } QSortFilterProxyModel* ProjectWidget::proxyModel() { return m_proxyModel; }