diff --git a/src/actionproxy.cpp b/src/actionproxy.cpp index 7a75323..2c8895f 100644 --- a/src/actionproxy.cpp +++ b/src/actionproxy.cpp @@ -1,116 +1,117 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 "actionproxy.h" #include #if 0 #include ActionProxy::ActionProxy(QObject* parent, QObject* receiver, const char* slot) : QObject(parent) , m_currentAction(0) , m_disabled(false) , m_checked(false) // , m_checkable(false) { if (receiver) connect(this, SIGNAL(triggered(bool)), receiver, slot); connect(this, &ActionProxy::toggled, this, &ActionProxy::handleToggled); } ActionProxy::~ActionProxy() { // if the view is closed... } void ActionProxy::registerAction(QAction* a) { if (a == m_currentAction) return; m_currentAction = a; a->setChecked(m_checked); a->setDisabled(m_disabled); a->setStatusTip(m_statusTip); m_keySequence = a->shortcut(); connect(a, SIGNAL(triggered(bool)), this, SIGNAL(triggered(bool))); connect(a, SIGNAL(toggled(bool)), this, SIGNAL(toggled(bool))); } void ActionProxy::unregisterAction(/*QAction**/) { disconnect(m_currentAction, SIGNAL(triggered(bool)), this, SIGNAL(triggered(bool))); disconnect(m_currentAction, SIGNAL(toggled(bool)), this, SIGNAL(toggled(bool))); m_currentAction->setStatusTip(QString()); m_currentAction = 0; } void ActionProxy::handleToggled(bool checked) { m_checked = checked; } void ActionProxy::setDisabled(bool disabled) { if (m_currentAction) m_currentAction->setDisabled(disabled); m_disabled = disabled; } void ActionProxy::setChecked(bool checked) { if (m_currentAction) m_currentAction->setChecked(checked); //handleToggled is called implicitly via signal/slot mechanism else m_checked = checked; } #endif void StatusBarProxy::insert(int key, const QString& str) { if (m_currentStatusBar) if (key < m_statusBarLabels.size()) m_statusBarLabels.at(key)->setText(str); QMap::insert(key, str); } void StatusBarProxy::registerStatusBar(QStatusBar* bar, const QVector& statusBarLabels) { m_currentStatusBar = bar; m_statusBarLabels = statusBarLabels; for (int i = 0; i < statusBarLabels.size(); i++) statusBarLabels.at(i)->setText(QString()); QMap::const_iterator i = constBegin(); while (i != constEnd()) { if (i.key() < statusBarLabels.size()) statusBarLabels.at(i.key())->setText(i.value()); ++i; } } diff --git a/src/actionproxy.h b/src/actionproxy.h index 1c029ca..1461915 100644 --- a/src/actionproxy.h +++ b/src/actionproxy.h @@ -1,116 +1,117 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 . **************************************************************************** */ #ifndef ACTIONPROXY_H #define ACTIONPROXY_H #include #include #include #include class QLabel; class QStatusBar; #if 0 /** * used for connecting qactions to subwindows: * forwards signals and saves/restores state on subwindow switch */ class ActionProxy: public QObject { Q_OBJECT public: ActionProxy(QObject* parent, QObject* receiver = 0, const char* slot = 0); ~ActionProxy(); void registerAction(QAction*); void unregisterAction(/*QAction**/); void setStatusTip(const QString& st) { m_statusTip = st; //for TM suggestions } QKeySequence shortcut() { return m_keySequence; };//for TM suggestions public slots: void setDisabled(bool); void setEnabled(bool enabled) { setDisabled(!enabled); } void setChecked(bool); private slots: void handleToggled(bool); signals: void triggered(bool = false); void toggled(bool); private: QAction* m_currentAction; bool m_disabled; bool m_checked; QString m_statusTip; QKeySequence m_keySequence; }; #endif class StatusBarProxy: public QMap { public: StatusBarProxy(): m_currentStatusBar(0) {} ~StatusBarProxy() {} void insert(int, const QString&); void registerStatusBar(QStatusBar*, const QVector& statusBarLabels); void unregisterStatusBar() { m_currentStatusBar = 0; } private: QStatusBar* m_currentStatusBar; QVector m_statusBarLabels; }; #define ID_STATUS_CURRENT 0 #define ID_STATUS_TOTAL 1 #define ID_STATUS_FUZZY 2 #define ID_STATUS_UNTRANS 3 #define ID_STATUS_ISFUZZY 4 #define ID_STATUS_PROGRESS 5 //#define TOTAL_ID_STATUSES 6 //#define ID_STATUS_READONLY 6 //#define ID_STATUS_CURSOR 7 #endif diff --git a/src/alttransview.cpp b/src/alttransview.cpp index ddb1d43..85b446e 100644 --- a/src/alttransview.cpp +++ b/src/alttransview.cpp @@ -1,310 +1,311 @@ /* **************************************************************************** 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 "alttransview.h" #include "lokalize_debug.h" #include "diff.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "xlifftextedit.h" #include "tmview.h" //TextBrowser #include "mergecatalog.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #include AltTransView::AltTransView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18nc("@title:window", "Alternate Translations"), parent) , m_browser(new TM::TextBrowser(this)) , m_catalog(catalog) , m_normTitle(i18nc("@title:window", "Alternate Translations")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_everShown(false) , m_actions(actions) { setObjectName(QStringLiteral("msgIdDiff")); setWidget(m_browser); hide(); m_browser->setReadOnly(true); m_browser->viewport()->setBackgroundRole(QPalette::Background); QTimer::singleShot(0, this, &AltTransView::initLater); } void AltTransView::initLater() { setAcceptDrops(true); KConfig config; KConfigGroup group(&config, "AltTransView"); m_everShown = group.readEntry("EverShown", false); QSignalMapper* signalMapper = new QSignalMapper(this); int i = m_actions.size(); while (--i >= 0) { connect(m_actions.at(i), &QAction::triggered, signalMapper, QOverload<>::of(&QSignalMapper::map)); signalMapper->setMapping(m_actions.at(i), i); } connect(signalMapper, QOverload::of(&QSignalMapper::mapped), this, &AltTransView::slotUseSuggestion); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &AltTransView::textInsertRequested); //connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &AltTransView::contextMenu); } AltTransView::~AltTransView() { } void AltTransView::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasUrls() && Catalog::extIsSupported(event->mimeData()->urls().first().path())) event->acceptProposedAction(); } void AltTransView::dropEvent(QDropEvent *event) { event->acceptProposedAction(); attachAltTransFile(event->mimeData()->urls().first().toLocalFile()); //update m_prevEntry.entry = -1; QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::attachAltTransFile(const QString& path) { MergeCatalog* altCat = new MergeCatalog(m_catalog, m_catalog, /*saveChanges*/false); altCat->loadFromUrl(path); m_catalog->attachAltTransCatalog(altCat); } void AltTransView::addAlternateTranslation(int entry, const QString& trans) { AltTrans altTrans; altTrans.target = trans; m_catalog->attachAltTrans(entry, altTrans); m_prevEntry = DocPos(); QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::fileLoaded() { m_prevEntry.entry = -1; QString absPath = m_catalog->url(); QString relPath = QDir(Project::instance()->projectDir()).relativeFilePath(absPath); QFileInfo info(Project::instance()->altTransDir() % '/' % relPath); if (info.canonicalFilePath() != absPath && info.exists()) attachAltTransFile(info.canonicalFilePath()); else qCWarning(LOKALIZE_LOG) << "alt trans file doesn't exist:" << Project::instance()->altTransDir() % '/' % relPath; } void AltTransView::slotNewEntryDisplayed(const DocPosition& pos) { m_entry = DocPos(pos); QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::process() { if (m_entry == m_prevEntry) return; if (m_catalog->numberOfEntries() <= m_entry.entry) return;//because of Qt::QueuedConnection m_prevEntry = m_entry; m_browser->clear(); m_entryPositions.clear(); const QVector& entries = m_catalog->altTrans(m_entry.toDocPosition()); m_entries = entries; if (entries.isEmpty()) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } if (!isVisible() && !Settings::altTransViewEverShownWithData()) { if (KMessageBox::questionYesNo(this, i18n("There is useful data available in Alternate Translations view.\n\n" "For Gettext PO files it displays difference between current source text " "and the source text corresponding to the fuzzy translation found by msgmerge when updating PO based on POT template.\n\n" "Do you want to show the view with the data?"), m_normTitle) == KMessageBox::Yes) show(); Settings::setAltTransViewEverShownWithData(true); } CatalogString source = m_catalog->sourceWithTags(m_entry.toDocPosition()); QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); int i = 0; int limit = entries.size(); forever { const AltTrans& entry = entries.at(i); QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); if (!entry.source.isEmpty()) { html += QStringLiteral("

"); QString result = userVisibleWordDiff(entry.source.string, source.string, Project::instance()->accel(), Project::instance()->markup()).toHtmlEscaped(); //result.replace("&","&"); //result.replace("<","<"); //result.replace(">",">"); result.replace(QStringLiteral("{KBABELADD}"), QStringLiteral("")); result.replace(QStringLiteral("{/KBABELADD}"), QStringLiteral("")); result.replace(QStringLiteral("{KBABELDEL}"), QStringLiteral("")); result.replace(QStringLiteral("{/KBABELDEL}"), QStringLiteral("")); result.replace(QStringLiteral("\\n"), QStringLiteral("\\n

")); html += result; html += QStringLiteral("
"); cur.insertHtml(html); html.clear(); } if (!entry.target.isEmpty()) { if (Q_LIKELY(i < m_actions.size())) { m_actions.at(i)->setStatusTip(entry.target.string); html += QString(QStringLiteral("[%1] ")).arg(m_actions.at(i)->shortcut().toString(QKeySequence::NativeText)); } else html += QStringLiteral("[ - ] "); cur.insertText(html); html.clear(); insertContent(cur, entry.target); } m_entryPositions.insert(cur.anchor(), i); html += i ? QStringLiteral("

") : QStringLiteral("

"); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } if (!m_everShown) { m_everShown = true; show(); KConfig config; KConfigGroup group(&config, "AltTransView"); group.writeEntry("EverShown", true); } } bool AltTransView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); if (m_entryPositions.isEmpty()) { QString tooltip = i18nc("@info:tooltip", "

Sometimes, if source text is changed, its translation becomes deprecated and is either marked as needing review (i.e. looses approval status), " "or (only in case of XLIFF file) moved to the alternate translations section accompanying the unit.

" "

This toolview also shows the difference between current source string and the previous source string, so that you can easily see which changes should be applied to existing translation to make it reflect current source.

" "

Double-clicking any word in this toolview inserts it into translation.

" "

Drop translation file onto this toolview to use it as a source for additional alternate translations.

" ); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } int block1 = m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block1 != block) qCWarning(LOKALIZE_LOG) << "block numbers don't match"; if (block >= m_entries.size()) return false; QString origin = m_entries.at(block).origin; if (origin.isEmpty()) return false; QString tooltip = i18nc("@info:tooltip", "Origin: %1", origin); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } return QWidget::event(event); } void AltTransView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; TM::TMEntry tmEntry; tmEntry.target = m_entries.at(i).target; CatalogString source = m_catalog->sourceWithTags(m_entry.toDocPosition()); tmEntry.diff = userVisibleWordDiff(m_entries.at(i).source.string, source.string, Project::instance()->accel(), Project::instance()->markup()); CatalogString target = TM::targetAdapted(tmEntry, source); qCWarning(LOKALIZE_LOG) << "0" << target.string; if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use alternate translation")); QString old = m_catalog->targetWithTags(m_entry.toDocPosition()).string; if (!old.isEmpty()) { //FIXME test! removeTargetSubstring(m_catalog, m_entry.toDocPosition(), 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_entry.toDocPosition(), target, 0); m_catalog->endMacro(); emit refreshRequested(); } diff --git a/src/alttransview.h b/src/alttransview.h index f522bad..fd04d92 100644 --- a/src/alttransview.h +++ b/src/alttransview.h @@ -1,85 +1,86 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2008 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 ALTTRANSVIEW_H #define ALTTRANSVIEW_H #define ALTTRANS_SHORTCUTS 9 #include "pos.h" #include "alttrans.h" #include namespace TM { class TextBrowser; } class Catalog; class QAction; class AltTransView: public QDockWidget { Q_OBJECT public: explicit AltTransView(QWidget*, Catalog*, const QVector&); ~AltTransView() override; public slots: void slotNewEntryDisplayed(const DocPosition&); void fileLoaded(); void attachAltTransFile(const QString&); void addAlternateTranslation(int entry, const QString&); private slots: //void contextMenu(const QPoint & pos); void process(); void initLater(); void slotUseSuggestion(int); signals: void refreshRequested(); void textInsertRequested(const QString&); private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent *event) override; bool event(QEvent *event) override; private: TM::TextBrowser* m_browser; Catalog* m_catalog; QString m_normTitle; QString m_hasInfoTitle; bool m_hasInfo; bool m_everShown; DocPos m_entry; DocPos m_prevEntry; QVector m_entries; QMap m_entryPositions; QVector m_actions;//need them to get shortcuts }; #endif diff --git a/src/binunitsview.cpp b/src/binunitsview.cpp index c5c5f98..c0510b3 100644 --- a/src/binunitsview.cpp +++ b/src/binunitsview.cpp @@ -1,213 +1,214 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-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 "binunitsview.h" #include "phaseswindow.h" //MyTreeView #include "catalog.h" #include "cmd.h" #include "project.h" #include #include #include #include #include #include //BEGIN BinUnitsModel BinUnitsModel::BinUnitsModel(Catalog* catalog, QObject* parent) : QAbstractListModel(parent) , m_catalog(catalog) { connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &BinUnitsModel::fileLoaded); connect(catalog, &Catalog::signalEntryModified, this, &BinUnitsModel::entryModified); connect(KDirWatch::self(), &KDirWatch::dirty, this, &BinUnitsModel::updateFile); } void BinUnitsModel::fileLoaded() { beginResetModel(); m_imageCache.clear(); endResetModel(); } void BinUnitsModel::entryModified(const DocPosition& pos) { if (pos.entry < m_catalog->numberOfEntries()) return; QModelIndex item = index(pos.entry - m_catalog->numberOfEntries(), TargetFilePath); emit dataChanged(item, item); } void BinUnitsModel::updateFile(QString path) { QString relPath = QDir(Project::instance()->projectDir()).relativeFilePath(path); DocPosition pos(m_catalog->numberOfEntries()); int limit = m_catalog->numberOfEntries() + m_catalog->binUnitsCount(); while (pos.entry < limit) { if (m_catalog->target(pos) == relPath || m_catalog->source(pos) == relPath) { int row = pos.entry - m_catalog->numberOfEntries(); m_imageCache.remove(relPath); emit dataChanged(index(row, SourceFilePath), index(row, TargetFilePath)); return; } pos.entry++; } } void BinUnitsModel::setTargetFilePath(int row, const QString& path) { DocPosition pos(row + m_catalog->numberOfEntries()); QString old = m_catalog->target(pos); if (!old.isEmpty()) { m_catalog->push(new DelTextCmd(m_catalog, pos, old)); m_imageCache.remove(old); } m_catalog->push(new InsTextCmd(m_catalog, pos, QDir(Project::instance()->projectDir()).relativeFilePath(path))); QModelIndex item = index(row, TargetFilePath); emit dataChanged(item, item); } int BinUnitsModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_catalog->binUnitsCount(); } QVariant BinUnitsModel::data(const QModelIndex& index, int role) const { if (role == Qt::DecorationRole) { DocPosition pos(index.row() + m_catalog->numberOfEntries()); if (index.column() < Approved) { QString path = index.column() == SourceFilePath ? m_catalog->source(pos) : m_catalog->target(pos); if (!m_imageCache.contains(path)) { QString absPath = Project::instance()->absolutePath(path); KDirWatch::self()->addFile(absPath); //TODO remember watched files to react only on them in dirty() signal handler m_imageCache.insert(path, QImage(absPath).scaled(128, 128, Qt::KeepAspectRatio)); } return m_imageCache.value(path); } } else if (role == Qt::TextAlignmentRole) return int(Qt::AlignLeft | Qt::AlignTop); if (role != Qt::DisplayRole) return QVariant(); static const char* noyes[] = {I18N_NOOP("no"), I18N_NOOP("yes")}; DocPosition pos(index.row() + m_catalog->numberOfEntries()); switch (index.column()) { case SourceFilePath: return m_catalog->source(pos); case TargetFilePath: return m_catalog->target(pos); case Approved: return noyes[m_catalog->isApproved(pos)]; } return QVariant(); } QVariant BinUnitsModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case SourceFilePath: return i18nc("@title:column", "Source"); case TargetFilePath: return i18nc("@title:column", "Target"); case Approved: return i18nc("@title:column", "Approved"); } return QVariant(); } //END BinUnitsModel BinUnitsView::BinUnitsView(Catalog* catalog, QWidget* parent) : QDockWidget(i18nc("@title toolview name", "Binary Units"), parent) , m_catalog(catalog) , m_model(new BinUnitsModel(catalog, this)) , m_view(new MyTreeView(this)) { setObjectName(QStringLiteral("binUnits")); hide(); setWidget(m_view); m_view->setModel(m_model); m_view->setRootIsDecorated(false); m_view->setAlternatingRowColors(true); m_view->viewport()->setBackgroundRole(QPalette::Background); connect(m_view, &MyTreeView::doubleClicked, this, &BinUnitsView::mouseDoubleClicked); connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &BinUnitsView::fileLoaded); } void BinUnitsView::fileLoaded() { setVisible(m_catalog->binUnitsCount()); } void BinUnitsView::selectUnit(const QString& id) { QModelIndex item = m_model->index(m_catalog->unitById(id) - m_catalog->numberOfEntries()); m_view->setCurrentIndex(item); m_view->scrollTo(item); show(); } void BinUnitsView::contextMenuEvent(QContextMenuEvent *event) { QModelIndex item = m_view->currentIndex(); if (!item.isValid()) return; QMenu menu; QAction* setTarget = menu.addAction(i18nc("@action:inmenu", "Set the file")); QAction* useSource = menu.addAction(i18nc("@action:inmenu", "Use source file")); // menu.addSeparator(); // QAction* openSource=menu.addAction(i18nc("@action:inmenu","Open source file in external program")); // QAction* openTarget=menu.addAction(i18nc("@action:inmenu","Open target file in external program")); QAction* result = menu.exec(event->globalPos()); if (!result) return; QString sourceFilePath = item.sibling(item.row(), BinUnitsModel::SourceFilePath).data().toString(); if (result == useSource) m_model->setTargetFilePath(item.row(), sourceFilePath); else if (result == setTarget) { QString targetFilePath = QFileDialog::getOpenFileName(this, QString(), Project::instance()->projectDir()); if (!targetFilePath.isEmpty()) m_model->setTargetFilePath(item.row(), targetFilePath); } event->accept(); } void BinUnitsView::mouseDoubleClicked(const QModelIndex& item) { //FIXME child processes don't notify us about changes ;( if (item.column() < BinUnitsModel::Approved) new KRun(QUrl::fromLocalFile(Project::instance()->absolutePath(item.data().toString())), this); } diff --git a/src/binunitsview.h b/src/binunitsview.h index a19645e..f054a7c 100644 --- a/src/binunitsview.h +++ b/src/binunitsview.h @@ -1,95 +1,96 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 BINUNITSVIEW_H #define BINUNITSVIEW_H class Catalog; class BinUnitsModel; class MyTreeView; #include "pos.h" #include #include #include class BinUnitsView: public QDockWidget { Q_OBJECT public: explicit BinUnitsView(Catalog* catalog, QWidget *parent); public slots: void selectUnit(const QString& id); private: void contextMenuEvent(QContextMenuEvent *event) override; private slots: void mouseDoubleClicked(const QModelIndex&); void fileLoaded(); private: Catalog* m_catalog; BinUnitsModel* m_model; MyTreeView* m_view; }; class BinUnitsModel: public QAbstractListModel { Q_OBJECT public: enum BinUnitsModelColumns { SourceFilePath = 0, TargetFilePath, Approved, ColumnCount }; BinUnitsModel(Catalog* catalog, QObject* parent); ~BinUnitsModel() {} int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent); return ColumnCount; } QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; void setTargetFilePath(int row, const QString&); private slots: void fileLoaded(); void entryModified(const DocPosition&); void updateFile(QString path); private: Catalog* m_catalog; mutable QHash m_imageCache; }; #endif // BINUNITSVIEW_H diff --git a/src/catalog/alttrans.h b/src/catalog/alttrans.h index 8f45b67..862fb57 100644 --- a/src/catalog/alttrans.h +++ b/src/catalog/alttrans.h @@ -1,48 +1,49 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 ALTTRANS_H #define ALTTRANS_H #include "catalogstring.h" #include "tmentry.h" struct AltTrans { ///@see http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#alttranstype enum Type {Proposal, PreviousVersion, Rejected, Reference, Accepted, Other}; Type type; CatalogString source; CatalogString target; short score; QString lang; QString origin; QString phase; AltTrans(const CatalogString& s = CatalogString(), const QString& o = QString()): type(Other), source(s), score(0), origin(o) {} }; #endif diff --git a/src/catalog/catalog.cpp b/src/catalog/catalog.cpp index b168898..79146c7 100644 --- a/src/catalog/catalog.cpp +++ b/src/catalog/catalog.cpp @@ -1,1067 +1,1068 @@ /* **************************************************************************** This file is part of Lokalize This file contains parts of KBabel code Copyright (C) 1999-2000 by Matthias Kiefer 2001-2005 by Stanislav Visnovsky 2006 by Nicolas Goutte 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "catalog.h" #include "catalog_private.h" #include "project.h" #include "projectmodel.h" //to notify about modification #include "catalogstorage.h" #include "gettextstorage.h" #include "gettextimport.h" #include "gettextexport.h" #include "xliffstorage.h" #include "tsstorage.h" #include "mergecatalog.h" #include "version.h" #include "prefs_lokalize.h" #include "jobs.h" #include "dbfilesmodel.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //QString Catalog::supportedMimeFilters("text/x-gettext-translation application/x-xliff application/x-linguist"); //" text/x-gettext-translation-template") QString Catalog::supportedFileTypes(bool includeTemplates) { QString sep = QStringLiteral(";;"); QString all = i18n("All supported files (*.po *.pot *.xlf *.xliff *.ts)") + sep; return all + (includeTemplates ? i18n("Gettext (*.po *.pot)") : i18n("Gettext (*.po)")) + sep + i18n("XLIFF (*.xlf *.xliff)") + sep + i18n("Linguist (*.ts)"); } static const QString extensions[] = {U(".po"), U(".pot"), U(".xlf"), U(".xliff"), U(".ts")}; static const char* const xliff_states[] = { I18N_NOOP("New"), I18N_NOOP("Needs translation"), I18N_NOOP("Needs full localization"), I18N_NOOP("Needs adaptation"), I18N_NOOP("Translated"), I18N_NOOP("Needs translation review"), I18N_NOOP("Needs full localization review"), I18N_NOOP("Needs adaptation review"), I18N_NOOP("Final"), I18N_NOOP("Signed-off") }; const char* const* Catalog::states() { return xliff_states; } QStringList Catalog::supportedExtensions() { QStringList result; int i = sizeof(extensions) / sizeof(QString); while (--i >= 0) result.append(extensions[i]); return result; } bool Catalog::extIsSupported(const QString& path) { QStringList ext = supportedExtensions(); int i = ext.size(); while (--i >= 0 && !path.endsWith(ext.at(i))) ; return i != -1; } Catalog::Catalog(QObject *parent) : QUndoStack(parent) , d(this) , m_storage(0) { //cause refresh events for files modified from lokalize itself aint delivered automatically connect(this, QOverload::of(&Catalog::signalFileSaved), Project::instance()->model(), QOverload::of(&ProjectModel::slotFileSaved), Qt::QueuedConnection); QTimer* t = &(d._autoSaveTimer); t->setInterval(2 * 60 * 1000); t->setSingleShot(false); connect(t, &QTimer::timeout, this, &Catalog::doAutoSave); connect(this, QOverload<>::of(&Catalog::signalFileSaved), t, QOverload<>::of(&QTimer::start)); connect(this, QOverload<>::of(&Catalog::signalFileLoaded), t, QOverload<>::of(&QTimer::start)); connect(this, &Catalog::indexChanged, this, &Catalog::setAutoSaveDirty); connect(Project::local(), &Project::configChanged, this, &Catalog::projectConfigChanged); } Catalog::~Catalog() { clear(); //delete m_storage; //deleted in clear(); } void Catalog::clear() { setIndex(cleanIndex());//to keep TM in sync QUndoStack::clear(); d._errorIndex.clear(); d._nonApprovedIndex.clear(); d._nonApprovedNonEmptyIndex.clear(); d._emptyIndex.clear(); delete m_storage; m_storage = 0; d._filePath.clear(); d._lastModifiedPos = DocPosition(); d._modifiedEntries.clear(); while (!d._altTransCatalogs.isEmpty()) d._altTransCatalogs.takeFirst()->deleteLater(); d._altTranslations.clear(); /* d.msgidDiffList.clear(); d.msgstr2MsgidDiffList.clear(); d.diffCache.clear(); */ } void Catalog::push(QUndoCommand* cmd) { generatePhaseForCatalogIfNeeded(this); QUndoStack::push(cmd); } //BEGIN STORAGE TRANSLATION int Catalog::capabilities() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->capabilities(); } int Catalog::numberOfEntries() const { if (Q_UNLIKELY(!m_storage)) return 0; return m_storage->size(); } static DocPosition alterForSinglePlural(const Catalog* th, DocPosition pos) { //if source lang is english (implied) and target lang has only 1 plural form (e.g. Chinese) if (Q_UNLIKELY(th->numberOfPluralForms() == 1 && th->isPlural(pos))) pos.form = 1; return pos; } QString Catalog::msgid(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->source(alterForSinglePlural(this, pos)); } QString Catalog::msgidWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->sourceWithPlurals(pos, truncateFirstLine); } QString Catalog::msgstr(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->target(pos); } QString Catalog::msgstrWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->targetWithPlurals(pos, truncateFirstLine); } CatalogString Catalog::sourceWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->sourceWithTags(alterForSinglePlural(this, pos)); } CatalogString Catalog::targetWithTags(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->targetWithTags(pos); } CatalogString Catalog::catalogString(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return CatalogString(); return m_storage->catalogString(pos.part == DocPosition::Source ? alterForSinglePlural(this, pos) : pos); } QVector Catalog::notes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->notes(pos); } QVector Catalog::developerNotes(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QVector(); return m_storage->developerNotes(pos); } Note Catalog::setNote(const DocPosition& pos, const Note& note) { if (Q_UNLIKELY(!m_storage)) return Note(); return m_storage->setNote(pos, note); } QStringList Catalog::noteAuthors() const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->noteAuthors(); } void Catalog::attachAltTransCatalog(Catalog* altCat) { d._altTransCatalogs.append(altCat); if (numberOfEntries() != altCat->numberOfEntries()) qCWarning(LOKALIZE_LOG) << altCat->url() << "has different number of entries"; } void Catalog::attachAltTrans(int entry, const AltTrans& trans) { d._altTranslations.insert(entry, trans); } QVector Catalog::altTrans(const DocPosition& pos) const { QVector result; if (m_storage) result = m_storage->altTrans(pos); foreach (Catalog* altCat, d._altTransCatalogs) { if (pos.entry >= altCat->numberOfEntries()) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because" << pos.entry << "<" << altCat->numberOfEntries(); continue; } if (altCat->source(pos) != source(pos)) { qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because s don't match"; continue; } QString target = altCat->msgstr(pos); if (!target.isEmpty() && altCat->isApproved(pos)) { result << AltTrans(); result.last().target = target; result.last().type = AltTrans::Reference; result.last().origin = altCat->url(); } } if (d._altTranslations.contains(pos.entry)) result << d._altTranslations.value(pos.entry); return result; } QStringList Catalog::sourceFiles(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->sourceFiles(pos); } QString Catalog::id(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->id(pos); } QStringList Catalog::context(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QStringList(); return m_storage->context(pos); } QString Catalog::setPhase(const DocPosition& pos, const QString& phase) { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->setPhase(pos, phase); } void Catalog::setActivePhase(const QString& phase, ProjectLocal::PersonRole role) { //qCDebug(LOKALIZE_LOG)<<"setting active phase"<size(); while (pos.entry < limit) { if (m_storage->isEmpty(pos)) d._emptyIndex << pos.entry; if (!isApproved(pos)) { d._nonApprovedIndex << pos.entry; if (!m_storage->isEmpty(pos)) { d._nonApprovedNonEmptyIndex << pos.entry; } } ++(pos.entry); } emit signalNumberOfFuzziesChanged(); emit signalNumberOfEmptyChanged(); } QString Catalog::phase(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->phase(pos); } Phase Catalog::phase(const QString& name) const { return m_storage->phase(name); } QList Catalog::allPhases() const { return m_storage->allPhases(); } QVector Catalog::phaseNotes(const QString& phase) const { return m_storage->phaseNotes(phase); } QVector Catalog::setPhaseNotes(const QString& phase, QVector notes) { return m_storage->setPhaseNotes(phase, notes); } QMap Catalog::allTools() const { return m_storage->allTools(); } bool Catalog::isPlural(uint index) const { return m_storage && m_storage->isPlural(DocPosition(index)); } bool Catalog::isApproved(uint index) const { if (Q_UNLIKELY(!m_storage)) return false; bool extendedStates = m_storage->capabilities()&ExtendedStates; return (extendedStates &&::isApproved(state(DocPosition(index)), activePhaseRole())) || (!extendedStates && m_storage->isApproved(DocPosition(index))); } TargetState Catalog::state(const DocPosition& pos) const { if (Q_UNLIKELY(!m_storage)) return NeedsTranslation; if (m_storage->capabilities()&ExtendedStates) return m_storage->state(pos); else return closestState(m_storage->isApproved(pos), activePhaseRole()); } bool Catalog::isEmpty(uint index) const { return m_storage && m_storage->isEmpty(DocPosition(index)); } bool Catalog::isEmpty(const DocPosition& pos) const { return m_storage && m_storage->isEmpty(pos); } bool Catalog::isEquivTrans(const DocPosition& pos) const { return m_storage && m_storage->isEquivTrans(pos); } int Catalog::binUnitsCount() const { return m_storage ? m_storage->binUnitsCount() : 0; } int Catalog::unitById(const QString& id) const { return m_storage ? m_storage->unitById(id) : 0; } QString Catalog::mimetype() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->mimetype(); } QString Catalog::fileType() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->fileType(); } CatalogType Catalog::type() { if (Q_UNLIKELY(!m_storage)) return Gettext; return m_storage->type(); } QString Catalog::sourceLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->sourceLangCode(); } QString Catalog::targetLangCode() const { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->targetLangCode(); } void Catalog::setTargetLangCode(const QString& targetLangCode) { if (Q_UNLIKELY(!m_storage)) return; bool notify = m_storage->targetLangCode() != targetLangCode; m_storage->setTargetLangCode(targetLangCode); if (notify) emit signalFileLoaded(); } //END STORAGE TRANSLATION //BEGIN OPEN/SAVE #define DOESNTEXIST -1 #define ISNTREADABLE -2 #define UNKNOWNFORMAT -3 KAutoSaveFile* Catalog::checkAutoSave(const QString& url) { KAutoSaveFile* autoSave = 0; QList staleFiles = KAutoSaveFile::staleFiles(QUrl::fromLocalFile(url)); foreach (KAutoSaveFile *stale, staleFiles) { if (stale->open(QIODevice::ReadOnly) && !autoSave) { autoSave = stale; autoSave->setParent(this); } else stale->deleteLater(); } if (autoSave) qCInfo(LOKALIZE_LOG) << "autoSave" << autoSave->fileName(); return autoSave; } int Catalog::loadFromUrl(const QString& filePath, const QString& saidUrl, int* fileSize, bool fast) { QFileInfo info(filePath); if (Q_UNLIKELY(!info.exists() || info.isDir())) return DOESNTEXIST; if (Q_UNLIKELY(!info.isReadable())) return ISNTREADABLE; bool readOnly = !info.isWritable(); QTime a; a.start(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) return ISNTREADABLE;//TODO CatalogStorage* storage = nullptr; if (filePath.endsWith(QLatin1String(".po")) || filePath.endsWith(QLatin1String(".pot"))) storage = new GettextCatalog::GettextStorage; else if (filePath.endsWith(QLatin1String(".xlf")) || filePath.endsWith(QLatin1String(".xliff"))) storage = new XliffStorage; else if (filePath.endsWith(QLatin1String(".ts"))) storage = new TsStorage; else { //try harder QTextStream in(&file); int i = 0; bool gettext = false; while (!in.atEnd() && ++i < 64 && !gettext) gettext = in.readLine().contains(QLatin1String("msgid")); if (gettext) storage = new GettextCatalog::GettextStorage; else return UNKNOWNFORMAT; } int line = storage->load(&file); file.close(); if (Q_UNLIKELY(line != 0 || (!storage->size() && (line == -1)))) { delete storage; return line; } if (a.elapsed() > 100) qCDebug(LOKALIZE_LOG) << filePath << "opened in" << a.elapsed(); //ok... clear(); //commit transaction m_storage = storage; updateApprovedEmptyIndexCache(); d._numberOfPluralForms = storage->numberOfPluralForms(); d._autoSaveDirty = true; d._readOnly = readOnly; d._filePath = saidUrl.isEmpty() ? filePath : saidUrl; //set some sane role, a real phase with a nmae will be created later with the first edit command setActivePhase(QString(), Project::local()->role()); if (!fast) { KAutoSaveFile* autoSave = checkAutoSave(d._filePath); d._autoSaveRecovered = autoSave; if (autoSave) { d._autoSave->deleteLater(); d._autoSave = autoSave; //restore 'modified' status for entries MergeCatalog* mergeCatalog = new MergeCatalog(this, this); int errorLine = mergeCatalog->loadFromUrl(autoSave->fileName()); if (Q_LIKELY(errorLine == 0)) mergeCatalog->copyToBaseCatalog(); mergeCatalog->deleteLater(); d._autoSave->close(); } else d._autoSave->setManagedFile(QUrl::fromLocalFile(d._filePath)); } if (fileSize) *fileSize = file.size(); emit signalFileLoaded(); emit signalFileLoaded(d._filePath); return 0; } bool Catalog::save() { return saveToUrl(d._filePath); } //this function is not called if QUndoStack::isClean() ! bool Catalog::saveToUrl(QString localFilePath) { if (Q_UNLIKELY(!m_storage)) return true; bool nameChanged = localFilePath.length(); if (Q_LIKELY(!nameChanged)) localFilePath = d._filePath; QString localPath = QFileInfo(localFilePath).absolutePath(); if (!QFileInfo::exists(localPath)) if (!QDir::root().mkpath(localPath)) return false; QFile file(localFilePath); if (Q_UNLIKELY(!file.open(QIODevice::WriteOnly))) //i18n("Wasn't able to open file %1",filename.ascii()); return false; bool belongsToProject = localFilePath.contains(Project::instance()->poDir()); if (Q_UNLIKELY(!m_storage->save(&file, belongsToProject))) return false; file.close(); d._autoSave->remove(); d._autoSaveRecovered = false; setClean(); //undo/redo if (nameChanged) { d._filePath = localFilePath; d._autoSave->setManagedFile(QUrl::fromLocalFile(localFilePath)); } //Settings::self()->setCurrentGroup("Bookmarks"); //Settings::self()->addItemIntList(d._filePath.url(),d._bookmarkIndex); emit signalFileSaved(); emit signalFileSaved(localFilePath); return true; /* else if (status==NO_PERMISSIONS) { if (KMessageBox::warningContinueCancel(this, i18n("You do not have permission to write to file:\n%1\n" "Do you want to save to another file or cancel?", _currentURL.prettyUrl()), i18n("Error"),KStandardGuiItem::save())==KMessageBox::Continue) return fileSaveAs(); } */ } void Catalog::doAutoSave() { if (isClean() || !(d._autoSaveDirty)) return; if (Q_UNLIKELY(!m_storage)) return; if (!d._autoSave->open(QIODevice::WriteOnly)) { emit signalFileAutoSaveFailed(d._autoSave->fileName()); return; } qCInfo(LOKALIZE_LOG) << "doAutoSave" << d._autoSave->fileName(); m_storage->save(d._autoSave); d._autoSave->close(); d._autoSaveDirty = false; } void Catalog::projectConfigChanged() { setActivePhase(activePhase(), Project::local()->role()); } QByteArray Catalog::contents() { QBuffer buf; buf.open(QIODevice::WriteOnly); m_storage->save(&buf); buf.close(); return buf.data(); } //END OPEN/SAVE /** * helper method to keep db in a good shape :) * called on * 1) entry switch * 2) automatic editing code like replace or undo/redo operation **/ static void updateDB( const QString& filePath, const QString& ctxt, const CatalogString& english, const CatalogString& newTarget, int form, bool approved, const QString& dbName //const DocPosition&,//for back tracking ) { TM::UpdateJob* j = new TM::UpdateJob(filePath, ctxt, english, newTarget, form, approved, dbName); TM::threadPool()->start(j); } //BEGIN UNDO/REDO const DocPosition& Catalog::undo() { QUndoStack::undo(); return d._lastModifiedPos; } const DocPosition& Catalog::redo() { QUndoStack::redo(); return d._lastModifiedPos; } void Catalog::flushUpdateDBBuffer() { if (!Settings::autoaddTM()) return; DocPosition pos = d._lastModifiedPos; if (pos.entry == -1 || pos.entry >= numberOfEntries()) { //nothing to flush //qCWarning(LOKALIZE_LOG)<<"nothing to flush or new file opened"; return; } QString dbName; if (Project::instance()->targetLangCode() == targetLangCode()) { dbName = Project::instance()->projectID(); } else { dbName = sourceLangCode() % '-' % targetLangCode(); qCInfo(LOKALIZE_LOG) << "updating" << dbName << "because target language of project db does not match" << Project::instance()->targetLangCode() << targetLangCode(); if (!TM::DBFilesModel::instance()->m_configurations.contains(dbName)) { TM::OpenDBJob* openDBJob = new TM::OpenDBJob(dbName, TM::Local, true); connect(openDBJob, &TM::OpenDBJob::done, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex); openDBJob->m_setParams = true; openDBJob->m_tmConfig.markup = Project::instance()->markup(); openDBJob->m_tmConfig.accel = Project::instance()->accel(); openDBJob->m_tmConfig.sourceLangCode = sourceLangCode(); openDBJob->m_tmConfig.targetLangCode = targetLangCode(); TM::DBFilesModel::instance()->openDB(openDBJob); } } int form = -1; if (isPlural(pos.entry)) form = pos.form; updateDB(url(), context(pos.entry).first(), sourceWithTags(pos), targetWithTags(pos), form, isApproved(pos.entry), dbName); d._lastModifiedPos = DocPosition(); } void Catalog::setLastModifiedPos(const DocPosition& pos) { if (pos.entry >= numberOfEntries()) //bin-units return; bool entryChanged = DocPos(d._lastModifiedPos) != DocPos(pos); if (entryChanged) flushUpdateDBBuffer(); d._lastModifiedPos = pos; } bool CatalogPrivate::addToEmptyIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos, bool alreadyEmpty) { if ((!pos.offset) && (storage->target(pos).isEmpty()) && (!alreadyEmpty)) { insertInList(_emptyIndex, pos.entry); return true; } return false; } void Catalog::targetDelete(const DocPosition& pos, int count) { if (Q_UNLIKELY(!m_storage)) return; bool alreadyEmpty = m_storage->isEmpty(pos); m_storage->targetDelete(pos, count); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); } bool CatalogPrivate::removeFromUntransIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos) { if ((!pos.offset) && (storage->isEmpty(pos))) { _emptyIndex.removeAll(pos.entry); return true; } return false; } void Catalog::targetInsert(const DocPosition& pos, const QString& arg) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsert(pos, arg); emit signalEntryModified(pos); } void Catalog::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { if (Q_UNLIKELY(!m_storage)) return; if (d.removeFromUntransIndexIfAppropriate(m_storage, pos)) emit signalNumberOfEmptyChanged(); m_storage->targetInsertTag(pos, tag); emit signalEntryModified(pos); } InlineTag Catalog::targetDeleteTag(const DocPosition& pos) { if (Q_UNLIKELY(!m_storage)) return InlineTag(); bool alreadyEmpty = m_storage->isEmpty(pos); InlineTag tag = m_storage->targetDeleteTag(pos); if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty)) emit signalNumberOfEmptyChanged(); emit signalEntryModified(pos); return tag; } void Catalog::setTarget(DocPosition pos, const CatalogString& s) { //TODO for case of markup present m_storage->setTarget(pos, s.string); } TargetState Catalog::setState(const DocPosition& pos, TargetState state) { bool extendedStates = m_storage && m_storage->capabilities()&ExtendedStates; bool approved =::isApproved(state, activePhaseRole()); if (Q_UNLIKELY(!m_storage || (extendedStates && m_storage->state(pos) == state) || (!extendedStates && m_storage->isApproved(pos) == approved))) return this->state(pos); TargetState prevState; if (extendedStates) { prevState = m_storage->setState(pos, state); d._statesIndex[prevState].removeAll(pos.entry); insertInList(d._statesIndex[state], pos.entry); } else { prevState = closestState(!approved, activePhaseRole()); m_storage->setApproved(pos, approved); } if (!approved) { insertInList(d._nonApprovedIndex, pos.entry); if (!m_storage->isEmpty(pos)) insertInList(d._nonApprovedNonEmptyIndex, pos.entry); } else { d._nonApprovedIndex.removeAll(pos.entry); d._nonApprovedNonEmptyIndex.removeAll(pos.entry); } emit signalNumberOfFuzziesChanged(); emit signalEntryModified(pos); return prevState; } Phase Catalog::updatePhase(const Phase& phase) { return m_storage->updatePhase(phase); } void Catalog::setEquivTrans(const DocPosition& pos, bool equivTrans) { if (m_storage) m_storage->setEquivTrans(pos, equivTrans); } bool Catalog::setModified(DocPos entry, bool modified) { if (modified) { if (d._modifiedEntries.contains(entry)) return false; d._modifiedEntries.insert(entry); } else d._modifiedEntries.remove(entry); return true; } bool Catalog::isModified(DocPos entry) const { return d._modifiedEntries.contains(entry); } bool Catalog::isModified(int entry) const { if (!isPlural(entry)) return isModified(DocPos(entry, 0)); int f = numberOfPluralForms(); while (--f >= 0) if (isModified(DocPos(entry, f))) return true; return false; } //END UNDO/REDO int findNextInList(const QLinkedList& list, int index) { int nextIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key > index)) { nextIndex = key; break; } } return nextIndex; } int findPrevInList(const QLinkedList& list, int index) { int prevIndex = -1; foreach (int key, list) { if (Q_UNLIKELY(key >= index)) break; prevIndex = key; } return prevIndex; } void insertInList(QLinkedList& list, int index) { QLinkedList::Iterator it = list.begin(); while (it != list.end() && index > *it) ++it; list.insert(it, index); } void Catalog::setBookmark(uint idx, bool set) { if (set) insertInList(d._bookmarkIndex, idx); else d._bookmarkIndex.removeAll(idx); } bool isApproved(TargetState state, ProjectLocal::PersonRole role) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state >= marginStates[role]; } bool isApproved(TargetState state) { static const TargetState marginStates[] = {Translated, Final, SignedOff}; return state == marginStates[0] || state == marginStates[1] || state == marginStates[2]; } TargetState closestState(bool approved, ProjectLocal::PersonRole role) { Q_ASSERT(role != ProjectLocal::Undefined); static const TargetState approvementStates[][3] = { {NeedsTranslation, NeedsReviewTranslation, NeedsReviewTranslation}, {Translated, Final, SignedOff} }; return approvementStates[approved][role]; } bool Catalog::isObsolete(int entry) const { if (Q_UNLIKELY(!m_storage)) return false; return m_storage->isObsolete(entry); } QString Catalog::originalOdfFilePath() { if (Q_UNLIKELY(!m_storage)) return QString(); return m_storage->originalOdfFilePath(); } void Catalog::setOriginalOdfFilePath(const QString& odfFilePath) { if (Q_UNLIKELY(!m_storage)) return; m_storage->setOriginalOdfFilePath(odfFilePath); } diff --git a/src/catalog/catalog.h b/src/catalog/catalog.h index cca7666..8180f4a 100644 --- a/src/catalog/catalog.h +++ b/src/catalog/catalog.h @@ -1,369 +1,370 @@ /***************************************************************************** This file is part of Lokalize This file contains parts of KBabel code Copyright (C) 1999-2000 by Matthias Kiefer 2001-2004 by Stanislav Visnovsky 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef CATALOG_H #define CATALOG_H #include "pos.h" #include "catalogstring.h" #include "catalogcapabilities.h" #include "note.h" #include "state.h" #include "phase.h" #include "alttrans.h" #include "catalog_private.h" class CatalogStorage; class MassReplaceJob; class KAutoSaveFile; #include namespace GettextCatalog { class CatalogImportPlugin; class CatalogExportPlugin; } bool isApproved(TargetState state, ProjectLocal::PersonRole role); bool isApproved(TargetState state); //disregarding Phase TargetState closestState(bool approved, ProjectLocal::PersonRole role); int findPrevInList(const QLinkedList& list, int index); int findNextInList(const QLinkedList& list, int index); void insertInList(QLinkedList& list, int index); // insert index in the right place in the list /** * This class represents a catalog * It uses CatalogStorage interface to work with catalogs in different formats * Also it defines all necessary functions to set and get the entries * * @short Wrapper class that represents a translation catalog * @author Nick Shaforostoff */ class Catalog: public QUndoStack { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.Lokalize.FileContainer") public: explicit Catalog(QObject* parent); virtual ~Catalog(); QString msgid(const DocPosition&) const; virtual QString msgstr(const DocPosition&) const; QString msgidWithPlurals(const DocPosition&, bool truncateFirstLine) const; QString msgstrWithPlurals(const DocPosition&, bool truncateFirstLine) const; static QStringList supportedExtensions(); static bool extIsSupported(const QString& path); static const char* const* states(); int capabilities() const; void push(QUndoCommand* cmd); public slots: //DBus interface QString source(const DocPosition& pos) const { return msgid(pos); } QString target(const DocPosition& pos) const { return msgstr(pos); } // used by XLIFF storage) CatalogString sourceWithTags(const DocPosition& pos) const; CatalogString targetWithTags(const DocPosition& pos) const; CatalogString catalogString(const DocPosition& pos) const; /** * @a pos.form is note number * @returns previous note contents, if any */ Note setNote(const DocPosition& pos, const Note& note); QVector notes(const DocPosition& pos) const; QVector developerNotes(const DocPosition& pos) const; QStringList noteAuthors() const; QVector altTrans(const DocPosition& pos) const; QStringList sourceFiles(const DocPosition& pos) const; //QString msgctxt(uint index) const; //the result is guaranteed to have at least 1 string QStringList context(const DocPosition& pos) const; QString id(const DocPosition& pos) const; ///@returns previous phase-name QString setPhase(const DocPosition& pos, const QString& phase); QString phase(const DocPosition& pos) const; QString activePhase() const { return d._phase; } ProjectLocal::PersonRole activePhaseRole() const { return d._phaseRole; } void setActivePhase(const QString& phase, ProjectLocal::PersonRole role = ProjectLocal::Approver); Phase phase(const QString& name) const; QList allPhases() const; QMap allTools() const; QVector phaseNotes(const QString& phase) const; ///@arg pos.entry - number of phase, @arg pos.form - number of note QVector setPhaseNotes(const QString& phase, QVector); bool isPlural(uint index) const; bool isPlural(const DocPosition& pos) const { return isPlural(pos.entry); } bool isApproved(uint index) const; bool isApproved(const DocPosition& pos) const { return isApproved(pos.entry); } TargetState state(const DocPosition& pos) const; bool isEquivTrans(const DocPosition&) const; ///@returns true if at least one form is untranslated bool isEmpty(uint index) const; bool isEmpty(const DocPosition&) const; bool isModified(DocPos entry) const; bool isModified(int entry) const; bool isObsolete(int entry) const; /// so DocPosition::entry may actually be < size()+binUnitsCount() int binUnitsCount() const; int unitById(const QString& id) const; bool isBookmarked(uint index) const { return d._bookmarkIndex.contains(index); } void setBookmark(uint, bool); int numberOfPluralForms() const { return d._numberOfPluralForms; } int numberOfEntries() const; int numberOfNonApproved() const { return d._nonApprovedNonEmptyIndex.size(); } int numberOfUntranslated() const { return d._emptyIndex.size(); } public: QString originalOdfFilePath(); void setOriginalOdfFilePath(const QString&); int firstFuzzyIndex() const { return d._nonApprovedIndex.isEmpty() ? numberOfEntries() : d._nonApprovedIndex.first(); } int lastFuzzyIndex() const { return d._nonApprovedIndex.isEmpty() ? -1 : d._nonApprovedIndex.last(); } int nextFuzzyIndex(uint index) const { return findNextInList(d._nonApprovedIndex, index); } int prevFuzzyIndex(uint index) const { return findPrevInList(d._nonApprovedIndex, index); } int firstUntranslatedIndex() const { return d._emptyIndex.isEmpty() ? numberOfEntries() : d._emptyIndex.first(); } int lastUntranslatedIndex() const { return d._emptyIndex.isEmpty() ? -1 : d._emptyIndex.last(); } int nextUntranslatedIndex(uint index) const { return findNextInList(d._emptyIndex, index); } int prevUntranslatedIndex(uint index) const { return findPrevInList(d._emptyIndex, index); } int firstBookmarkIndex() const { return d._bookmarkIndex.isEmpty() ? numberOfEntries() : d._bookmarkIndex.first(); } int lastBookmarkIndex() const { return d._bookmarkIndex.isEmpty() ? -1 : d._bookmarkIndex.last(); } int nextBookmarkIndex(uint index) const { return findNextInList(d._bookmarkIndex, index); } int prevBookmarkIndex(uint index) const { return findPrevInList(d._bookmarkIndex, index); } bool autoSaveRecovered() { return d._autoSaveRecovered; } public: void clear(); bool isEmpty() { return !m_storage; } bool isReadOnly() { return d._readOnly; } void attachAltTransCatalog(Catalog*); void attachAltTrans(int entry, const AltTrans& trans); virtual const DocPosition& undo(); virtual const DocPosition& redo(); void setTarget(DocPosition pos, const CatalogString& s); //for batch use only! //void setErrorIndex(const QList& errors){d._errorIndex=errors;} void setUrl(const QString& u) { d._filePath = u; //used for template load } public slots: //DBus interface const QString& url() const { return d._filePath; } ///@returns 0 if success, >0 erroneous line (parsing error) int loadFromUrl(const QString& url, const QString& saidUrl = QString(), int* fileSize = nullptr, bool fast = false); bool saveToUrl(QString url); bool save(); QByteArray contents(); QString mimetype(); QString fileType(); CatalogType type(); QString sourceLangCode() const; QString targetLangCode() const; void setTargetLangCode(const QString& targetLangCode); /** * updates DB for _posBuffer and accompanying _originalForLastModified */ void flushUpdateDBBuffer(); protected: virtual KAutoSaveFile* checkAutoSave(const QString& url); protected slots: void doAutoSave(); void setAutoSaveDirty() { d._autoSaveDirty = true; } void projectConfigChanged(); protected: /** * (EDITING) * accessed from undo/redo code * called _BEFORE_ modification */ void setLastModifiedPos(const DocPosition&); /** * (EDITING) * accessed from undo/redo code * accessed from mergeCatalog) * it _does_ check if action should be taken */ void setApproved(const DocPosition& pos, bool approved); void targetDelete(const DocPosition& pos, int count); void targetInsert(const DocPosition& pos, const QString& arg); InlineTag targetDeleteTag(const DocPosition& pos); void targetInsertTag(const DocPosition& pos, const InlineTag& tag); TargetState setState(const DocPosition& pos, TargetState state); Phase updatePhase(const Phase& phase); void setEquivTrans(const DocPosition&, bool equivTrans); /// @returns true if entry wasn't modified before bool setModified(DocPos entry, bool modif); void updateApprovedEmptyIndexCache(); protected: CatalogPrivate d; CatalogStorage *m_storage; friend class GettextCatalog::CatalogImportPlugin; friend class GettextCatalog::CatalogExportPlugin; friend class LokalizeUnitCmd; friend class InsTextCmd; friend class DelTextCmd; friend class InsTagCmd; friend class DelTagCmd; friend class SetStateCmd; friend class SetNoteCmd; friend class UpdatePhaseCmd; friend class MergeCatalog; friend class SetEquivTransCmd; friend class MassReplaceJob; public: //static QString supportedMimeFilters; static QString supportedFileTypes(bool includeTemplates = true); signals: void signalEntryModified(const DocPosition&); void activePhaseChanged(); void signalNumberOfFuzziesChanged(); void signalNumberOfEmptyChanged(); Q_SCRIPTABLE void signalFileLoaded(); void signalFileLoaded(const QString&); Q_SCRIPTABLE void signalFileSaved(); void signalFileSaved(const QString&); void signalFileAutoSaveFailed(const QString&); }; #endif diff --git a/src/catalog/catalog_private.h b/src/catalog/catalog_private.h index c638f9c..182994c 100644 --- a/src/catalog/catalog_private.h +++ b/src/catalog/catalog_private.h @@ -1,126 +1,127 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer 2001-2004 by Stanislav Visnovsky 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef CATALOGPRIVATE_H #define CATALOGPRIVATE_H #include "projectlocal.h" #include "state.h" #include "pos.h" #include "alttrans.h" #include #include #include #include #include #include #include #include class QTextCodec; class CatalogStorage; class Catalog; class CatalogPrivate { public: /** url of the po-file, that belongs to this catalog */ QString _filePath; QString _packageName; QString _packageDir; /** identification string for used import filter*/ QString _importID; QTextCodec *fileCodec; int _numberOfPluralForms; QTimer _autoSaveTimer; KAutoSaveFile* _autoSave; bool _autoSaveDirty; bool _autoSaveRecovered; bool _readOnly; //for wrapping short _maxLineLength; QLinkedList _nonApprovedIndex; QLinkedList _nonApprovedNonEmptyIndex; QLinkedList _emptyIndex; QLinkedList _errorIndex; QLinkedList _bookmarkIndex; QVector< QLinkedList > _statesIndex; QLinkedList _altTransCatalogs; QMap _altTranslations; //for undo/redo //keeps pos of the entry that was last modified DocPosition _lastModifiedPos; QSet _modifiedEntries;//just for the nice gui QString _phase; ProjectLocal::PersonRole _phaseRole; explicit CatalogPrivate(QObject* parent) : fileCodec(0) , _numberOfPluralForms(-1) , _autoSave(new KAutoSaveFile(parent)) , _autoSaveDirty(true) , _autoSaveRecovered(false) , _readOnly(false) , _maxLineLength(80) , _phaseRole(ProjectLocal::Undefined) { Q_UNUSED(parent) _statesIndex.resize(StateCount); } bool addToEmptyIndexIfAppropriate(CatalogStorage*, const DocPosition& pos, bool alreadyEmpty); bool removeFromUntransIndexIfAppropriate(CatalogStorage*, const DocPosition& pos); }; #endif //CatalogPrivate_H diff --git a/src/catalog/catalogcapabilities.h b/src/catalog/catalogcapabilities.h index 041ae82..615c391 100644 --- a/src/catalog/catalogcapabilities.h +++ b/src/catalog/catalogcapabilities.h @@ -1,41 +1,42 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 CATALOGCAPABILITIES_H #define CATALOGCAPABILITIES_H enum CatalogCapabilities { KeepsNoteAuthors = 1, MultipleNotes = 2, Phases = 4, ExtendedStates = 8, Tags = 16 }; enum CatalogType { Gettext, Xliff, Ts }; #endif diff --git a/src/catalog/catalogstorage.h b/src/catalog/catalogstorage.h index c118755..ca86e84 100644 --- a/src/catalog/catalogstorage.h +++ b/src/catalog/catalogstorage.h @@ -1,274 +1,275 @@ /* Copyright 2008-2009 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CATALOGSTORAGE_H #define CATALOGSTORAGE_H #include "pos.h" #include "catalogstring.h" #include "note.h" #include "state.h" #include "phase.h" #include "alttrans.h" #include "catalogcapabilities.h" #include #include /** * Abstract interface for storage of translation file * * format-specific elements like \" for gettext PO should be eliminated * * @short Abstract interface for storage of translation file * @author Nick Shaforostoff */ class CatalogStorage { public: CatalogStorage(); virtual ~CatalogStorage(); virtual int capabilities() const = 0; virtual int load(QIODevice* device) = 0; virtual bool save(QIODevice* device, bool belongsToProject = false) = 0; virtual int size() const = 0; int numberOfEntries()const { return size(); } int numberOfPluralForms() const { return m_numberOfPluralForms; } /** * flat-model interface (ignores XLIFF grouping) * * format-specific texts like \" for gettext PO should be eliminated **/ virtual QString source(const DocPosition& pos) const = 0; virtual QString target(const DocPosition& pos) const = 0; virtual QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const = 0; virtual QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const = 0; virtual CatalogString sourceWithTags(DocPosition pos) const = 0; virtual CatalogString targetWithTags(DocPosition pos) const = 0; virtual CatalogString catalogString(const DocPosition& pos) const = 0; /** * edit operations used by undo/redo system and sync-mode **/ virtual void targetDelete(const DocPosition& pos, int count) = 0; virtual void targetInsert(const DocPosition& pos, const QString& arg) = 0; virtual void setTarget(const DocPosition& pos, const QString& arg) = 0; //called for mergeCatalog TODO switch to CatalogString virtual void targetInsertTag(const DocPosition&, const InlineTag&) {} virtual InlineTag targetDeleteTag(const DocPosition&) { return InlineTag(); } virtual Phase updatePhase(const Phase&) { return Phase(); } virtual QList allPhases() const { return QList(); } virtual QMap allTools() const { return QMap(); } /// all plural forms. pos.form doesn't matter virtual QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const = 0; virtual QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const = 0; virtual QVector altTrans(const DocPosition& pos) const = 0; virtual QVector notes(const DocPosition& pos) const = 0; virtual Note setNote(DocPosition pos, const Note& note) = 0; virtual QStringList noteAuthors() const { return QStringList(); } virtual QVector developerNotes(const DocPosition& pos) const = 0; virtual QStringList sourceFiles(const DocPosition& pos) const = 0; virtual QString setPhase(const DocPosition& pos, const QString& phase) { Q_UNUSED(pos); Q_UNUSED(phase); return QString(); } virtual QString phase(const DocPosition& pos) const { Q_UNUSED(pos); return QString(); } virtual Phase phase(const QString& name) const { Q_UNUSED(name); return Phase(); } virtual QVector phaseNotes(const QString& phase) const { Q_UNUSED(phase); return QVector(); } virtual QVector setPhaseNotes(const QString& phase, QVector notes) { Q_UNUSED(phase); Q_UNUSED(notes); return QVector(); } //the result must be guaranteed to have at least 1 string virtual QStringList context(const DocPosition&) const = 0; //DocPosition.form - number of //virtual QString context(const DocPosition&) const=0; //virtual int contextCount(const DocPosition&) const=0; /** * user-invisible data for matching, e.g. during TM database lookup * it is comprised of several strings * * database stores them and thus it is possible to * fuzzy-match 'matchData' later * * it is responsibility of CatalogStorage implementations to * separate/assemble the list properly according to the format specifics * * pos.form doesn't matter **/ virtual QStringList matchData(const DocPosition&) const = 0; /** * entry id unique for this file * * pos.form doesn't matter **/ virtual QString id(const DocPosition&) const = 0; virtual bool isPlural(const DocPosition&) const = 0; virtual bool isEmpty(const DocPosition&) const = 0; virtual bool isEquivTrans(const DocPosition&) const { return true; } virtual void setEquivTrans(const DocPosition&, bool equivTrans) { Q_UNUSED(equivTrans) } virtual bool isApproved(const DocPosition&) const { return true; } virtual void setApproved(const DocPosition&, bool approved) { Q_UNUSED(approved) } virtual TargetState state(const DocPosition&) const { return New; } virtual TargetState setState(const DocPosition&, TargetState) { return New; } virtual bool isObsolete(int entry) const { Q_UNUSED(entry) return false; } virtual bool isTranslateable(int entry) const { Q_UNUSED(entry) return true; } virtual int binUnitsCount() const { return 0; } virtual int unitById(const QString& id) const { Q_UNUSED(id); return 0; } const QString& url() const { return m_url; } void setUrl(const QString& u) { m_url = u; //TODO } virtual QString mimetype() const = 0; virtual QString fileType() const = 0; virtual CatalogType type() const = 0; virtual QString originalOdfFilePath() { return QString(); } virtual void setOriginalOdfFilePath(const QString&) {} QString sourceLangCode() const { return m_sourceLangCode; } QString targetLangCode() const { return m_targetLangCode; } virtual void setTargetLangCode(const QString& langCode) { m_targetLangCode = langCode; } protected: QString m_url; QString m_sourceLangCode; QString m_targetLangCode; int m_numberOfPluralForms; }; inline CatalogStorage::CatalogStorage() : m_sourceLangCode(QStringLiteral("en_US")) , m_numberOfPluralForms(0) { } inline CatalogStorage::~CatalogStorage() { } #endif diff --git a/src/catalog/catalogstring.cpp b/src/catalog/catalogstring.cpp index 8513d6d..e3bdf80 100644 --- a/src/catalog/catalogstring.cpp +++ b/src/catalog/catalogstring.cpp @@ -1,325 +1,326 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 "catalogstring.h" #include "lokalize_debug.h" #include const char* InlineTag::getElementName(InlineElement type) { static const char* inlineElementNames[(int)InlineElementCount] = { "_unknown", "bpt", "ept", "ph", "it", //"_NEVERSHOULDBECHOSEN", "mrk", "g", "sub", "_NEVERSHOULDBECHOSEN", "x", "bx", "ex" }; return inlineElementNames[(int)type]; } InlineTag InlineTag::getPlaceholder() const { InlineTag tagRange = *this; tagRange.start = -1; tagRange.end = -1; return tagRange; } InlineTag::InlineElement InlineTag::getElementType(const QByteArray& tag) { int i = InlineTag::InlineElementCount; while (--i > 0) if (getElementName(InlineElement(i)) == tag) break; return InlineElement(i); } QString InlineTag::displayName() const { static const char* inlineElementNames[(int)InlineElementCount] = { "_unknown", I18N_NOOP2("XLIFF inline tag name", "Start of paired tag"), I18N_NOOP2("XLIFF inline tag name", "End of paired tag"), I18N_NOOP2("XLIFF inline tag name", "Stand-alone tag"), I18N_NOOP2("XLIFF inline tag name", "Isolated tag"), //"_NEVERSHOULDBECHOSEN", I18N_NOOP2("XLIFF inline tag name", "Marker"), I18N_NOOP2("XLIFF inline tag name", "Generic group placeholder"), I18N_NOOP2("XLIFF inline tag name", "Sub-flow"), "_NEVERSHOULDBECHOSEN", I18N_NOOP2("XLIFF inline tag name", "Generic placeholder"), I18N_NOOP2("XLIFF inline tag name", "Start of paired placeholder"), I18N_NOOP2("XLIFF inline tag name", "End of paired placeholder") }; QString result = i18nc("XLIFF inline tag name", inlineElementNames[type]); if (type == mrk) { static const char* mrkTypes[] = { "abbrev", "abbreviated-form", "abbreviation", "acronym", "appellation", "collocation", "common-name", "datetime", "equation", "expanded-form", "formula", "head-term", "initialism", "international-scientific-term", "internationalism", "logical-expression", "materials-management-unit", "name", "near-synonym", "part-number", "phrase", "phraseological-unit", "protected", "romanized-form", "seg", "set-phrase", "short-form", "sku", "standard-text", "symbol", "synonym", "synonymous-phrase", "term", "transcribed-form", "transliterated-form", "truncated-term", "variant" }; static const char* mrkTypeNames[] = { I18N_NOOP2("XLIFF mark type", "abbreviation"), I18N_NOOP2("XLIFF mark type", "abbreviated form: a term resulting from the omission of any part of the full term while designating the same concept"), I18N_NOOP2("XLIFF mark type", "abbreviation: an abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective')"), I18N_NOOP2("XLIFF mark type", "acronym: an abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging')"), I18N_NOOP2("XLIFF mark type", "appellation: a proper-name term, such as the name of an agency or other proper entity"), I18N_NOOP2("XLIFF mark type", "collocation: a recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another"), I18N_NOOP2("XLIFF mark type", "common name: a synonym for an international scientific term that is used in general discourse in a given language"), I18N_NOOP2("XLIFF mark type", "date and/or time"), I18N_NOOP2("XLIFF mark type", "equation: an expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign"), I18N_NOOP2("XLIFF mark type", "expanded form: The complete representation of a term for which there is an abbreviated form"), I18N_NOOP2("XLIFF mark type", "formula: figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula"), I18N_NOOP2("XLIFF mark type", "head term: the concept designation that has been chosen to head a terminological record"), I18N_NOOP2("XLIFF mark type", "initialism: an abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy')"), I18N_NOOP2("XLIFF mark type", "international scientific term: a term that is part of an international scientific nomenclature as adopted by an appropriate scientific body"), I18N_NOOP2("XLIFF mark type", "internationalism: a term that has the same or nearly identical orthographic or phonemic form in many languages"), I18N_NOOP2("XLIFF mark type", "logical expression: an expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like"), I18N_NOOP2("XLIFF mark type", "materials management unit: a unit to track object"), I18N_NOOP2("XLIFF mark type", "name"), I18N_NOOP2("XLIFF mark type", "near synonym: a term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others"), I18N_NOOP2("XLIFF mark type", "part number: a unique alphanumeric designation assigned to an object in a manufacturing system"), I18N_NOOP2("XLIFF mark type", "phrase"), I18N_NOOP2("XLIFF mark type", "phraseological: a group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase"), I18N_NOOP2("XLIFF mark type", "protected: the marked text should not be translated"), I18N_NOOP2("XLIFF mark type", "romanized form: a form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet"), I18N_NOOP2("XLIFF mark type", "segment: the marked text represents a segment"), I18N_NOOP2("XLIFF mark type", "set phrase: a fixed, lexicalized phrase"), I18N_NOOP2("XLIFF mark type", "short form: a variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs')"), I18N_NOOP2("XLIFF mark type", "stock keeping unit: an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system"), I18N_NOOP2("XLIFF mark type", "standard text: a fixed chunk of recurring text"), I18N_NOOP2("XLIFF mark type", "symbol: a designation of a concept by letters, numerals, pictograms or any combination thereof"), I18N_NOOP2("XLIFF mark type", "synonym: a term that represents the same or a very similar concept as the main entry term in a term entry"), I18N_NOOP2("XLIFF mark type", "synonymous phrase: phraseological unit in a language that expresses the same semantic content as another phrase in that same language"), I18N_NOOP2("XLIFF mark type", "term"), I18N_NOOP2("XLIFF mark type", "transcribed form: a form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted"), I18N_NOOP2("XLIFF mark type", "transliterated form: a form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system"), I18N_NOOP2("XLIFF mark type", "truncated term: an abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza')"), I18N_NOOP2("XLIFF mark type", "variant: one of the alternate forms of a term") }; int i = sizeof(mrkTypes) / sizeof(char*); while (--i >= 0 && mrkTypes[i] != id) ; if (i != -1) { result = i18nc("XLIFF mark type", mrkTypeNames[i]); if (!result.isEmpty()) result[0] = result.at(0).toUpper(); } } if (!ctype.isEmpty()) result += " (" + ctype + ')'; return result; } QMap CatalogString::tagIdToIndex() const { QMap result; int index = 0; int count = tags.size(); for (int i = 0; i < count; ++i) { if (!result.contains(tags.at(i).id)) result.insert(tags.at(i).id, index++); } return result; } QByteArray CatalogString::tagsAsByteArray()const { QByteArray result; if (tags.size()) { QDataStream stream(&result, QIODevice::WriteOnly); stream << tags; } return result; } CatalogString::CatalogString(QString str, QByteArray tagsByteArray) : string(str) { if (tagsByteArray.size()) { QDataStream stream(tagsByteArray); stream >> tags; } } static void adjustTags(QList& tags, int position, int value) { int i = tags.size(); while (--i >= 0) { InlineTag& t = tags[i]; if (t.start > position) t.start += value; if (t.end >= position) //cases when strict > is needed? t.end += value; } } void CatalogString::remove(int position, int len) { string.remove(position, len); adjustTags(tags, position, -len); } void CatalogString::insert(int position, const QString& str) { string.insert(position, str); adjustTags(tags, position, str.size()); } QDataStream &operator<<(QDataStream &out, const InlineTag &t) { return out << int(t.type) << t.start << t.end << t.id; } QDataStream &operator>>(QDataStream &in, InlineTag &t) { int type; in >> type >> t.start >> t.end >> t.id; t.type = InlineTag::InlineElement(type); return in; } QDataStream &operator<<(QDataStream &out, const CatalogString &myObj) { return out << myObj.string << myObj.tags; } QDataStream &operator>>(QDataStream &in, CatalogString &myObj) { return in >> myObj.string >> myObj.tags; } void adaptCatalogString(CatalogString& target, const CatalogString& ref) { //qCWarning(LOKALIZE_LOG) << "HERE" << target.string; QHash id2tagIndex; QMultiMap tagType2tagIndex; int i = ref.tags.size(); while (--i >= 0) { const InlineTag& t = ref.tags.at(i); id2tagIndex.insert(t.id, i); tagType2tagIndex.insert(t.type, i); qCWarning(LOKALIZE_LOG) << "inserting" << t.id << t.type << i; } QList oldTags = target.tags; target.tags.clear(); //we actually walking from beginning to end: qSort(oldTags.begin(), oldTags.end(), qGreater()); i = oldTags.size(); while (--i >= 0) { const InlineTag& targetTag = oldTags.at(i); if (id2tagIndex.contains(targetTag.id)) { qCWarning(LOKALIZE_LOG) << "matched" << targetTag.id << i; target.tags.append(targetTag); tagType2tagIndex.remove(targetTag.type, id2tagIndex.take(targetTag.id)); oldTags.removeAt(i); } } //qCWarning(LOKALIZE_LOG) << "HERE 0" << target.string; //now all the tags left have to ID (exact) matches i = oldTags.size(); while (--i >= 0) { InlineTag targetTag = oldTags.at(i); if (tagType2tagIndex.contains(targetTag.type)) { //try to match by position //we're _taking_ first so the next one becomes new 'first' for the next time. QList possibleRefMatches; foreach (int i, tagType2tagIndex.values(targetTag.type)) possibleRefMatches << ref.tags.at(i); qSort(possibleRefMatches); qCWarning(LOKALIZE_LOG) << "setting id:" << targetTag.id << possibleRefMatches.first().id; targetTag.id = possibleRefMatches.first().id; target.tags.append(targetTag); qCWarning(LOKALIZE_LOG) << "id??:" << targetTag.id << target.tags.first().id; tagType2tagIndex.remove(targetTag.type, id2tagIndex.take(targetTag.id)); oldTags.removeAt(i); } } //qCWarning(LOKALIZE_LOG) << "HERE 1" << target.string; //now walk through unmatched tags and properly remove them. foreach (const InlineTag& tag, oldTags) { if (tag.isPaired()) target.remove(tag.end, 1); target.remove(tag.start, 1); } //qCWarning(LOKALIZE_LOG) << "HERE 2" << target.string; } diff --git a/src/catalog/catalogstring.h b/src/catalog/catalogstring.h index 31509bf..6ee288c 100644 --- a/src/catalog/catalogstring.h +++ b/src/catalog/catalogstring.h @@ -1,183 +1,184 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 CATALOGSTRING_H #define CATALOGSTRING_H #include #include #include #include //#define TAGRANGE_IMAGE_SYMBOL 65532 #define TAGRANGE_IMAGE_SYMBOL QChar::ObjectReplacementCharacter /** * data structure used to pass info about inline elements * a XLIFF tag is represented by a TAGRANGE_IMAGE_SYMBOL in the 'plainttext' * and a struct TagRange * * describes which tag is behind TAGRANGE_IMAGE_SYMBOL char * (or chars -- starting and ending) in source or target string * start==end for non-paired tags */ struct InlineTag { //sub = can contain -flow tag //recursive = can contain other inline markup tags ///@see http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html enum InlineElement { _unknown, bpt, //sub ept, //sub ph, //sub it, //sub //_subContainersDelimiter, mrk, //recursive, no id g, //recursive sub, //recursive, no id _pairedXmlTagDelimiter, x, //empty bx, //empty ex, //empty InlineElementCount }; int start; int end; InlineElement type; QString id; QString xid; QString equivText; QString ctype; explicit InlineTag(): start(-1), end(-1), type(_unknown) {} InlineTag(int start_, int end_, InlineElement type_, QString id_ = QString(), QString xid_ = QString(), QString equivText_ = QString(), QString ctype_ = QString()) : start(start_), end(end_), type(type_), id(id_), xid(xid_), equivText(equivText_), ctype(ctype_) {} /** * for situations when target doesn't contain tag * (of the same type and with the same id) from source * true means that the object corresponds to some tag in source, * but target does not contain it. * * @see getPlaceholder() */ bool isEmpty()const { return start == -1; } /** * used to denote tag that doesn't present in target, * to have parallel numbering in view * * @returns TagRange object prototype to be inserted into target * @see isEmpty() */ InlineTag getPlaceholder() const; ///@returns 0 if type is unknown static InlineElement getElementType(const QByteArray&); static const char* getElementName(InlineElement type); const char* getElementName()const { return getElementName(type); } const char* name()const { return getElementName(); } static bool isPaired(InlineElement type) { return type < InlineTag::_pairedXmlTagDelimiter; } bool isPaired()const { return isPaired(type); } QString displayName() const; bool operator<(const InlineTag& other)const { return start < other.start; } }; Q_DECLARE_METATYPE(InlineTag) Q_DECLARE_METATYPE(QList) /** * data structure used to pass info about inline elements * a XLIFF tag is represented by a TAGRANGE_IMAGE_SYMBOL in the 'plainttext' * and a struct TagRange * * string has each XLIFF markup tag represented by 1 symbol * ranges is set to list describing which tag (type, id) at which position */ struct CatalogString { QString string; QList tags; CatalogString() {} CatalogString(QString str): string(str) {} CatalogString(QString str, QByteArray tagsByteArray); QMap tagIdToIndex() const; //assigns same indexes for tags with same ids QByteArray tagsAsByteArray()const; void remove(int position, int len); void insert(int position, const QString& str); void replace(int position, int len, const QString& str) { remove(position, len); insert(position, str); } void clear() { string.clear(); tags.clear(); } bool isEmpty() const { return string.isEmpty(); } }; Q_DECLARE_METATYPE(CatalogString) #include QDataStream &operator<<(QDataStream &out, const InlineTag &myObj); QDataStream &operator>>(QDataStream &in, InlineTag &myObj); QDataStream &operator<<(QDataStream &out, const CatalogString &myObj); QDataStream &operator>>(QDataStream &in, CatalogString &myObj); /// prepares @arg target for using it as @arg ref translation void adaptCatalogString(CatalogString& target, const CatalogString& ref); #endif diff --git a/src/catalog/cmd.cpp b/src/catalog/cmd.cpp index 35b916a..94d9fc3 100644 --- a/src/catalog/cmd.cpp +++ b/src/catalog/cmd.cpp @@ -1,467 +1,468 @@ /* **************************************************************************** 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 "cmd.h" #include "lokalize_debug.h" #include #include "catalog_private.h" #include "catalogitem_private.h" #include "catalog.h" #include "project.h" #include //BEGIN LokalizeUnitCmd LokalizeUnitCmd::LokalizeUnitCmd(Catalog *catalog, const DocPosition& pos, const QString& name = QString()) : QUndoCommand(name) , _catalog(catalog) , _pos(pos) , _firstModificationForThisEntry(false) {} static QString setPhaseForPart(Catalog* catalog, const QString& phase, DocPosition phasePos, DocPosition::Part part) { phasePos.part = part; return catalog->setPhase(phasePos, phase); } void LokalizeUnitCmd::redo() { setJumpingPos(); doRedo(); _firstModificationForThisEntry = _catalog->setModified(DocPos(_pos), true); // _prevPhase=setPhaseForPart(_catalog,_catalog->activePhase(),_pos,DocPosition::UndefPart); } void LokalizeUnitCmd::undo() { setJumpingPos(); doUndo(); if (_firstModificationForThisEntry) _catalog->setModified(DocPos(_pos), false); // setPhaseForPart(_catalog,_prevPhase,_pos,DocPosition::UndefPart); } void LokalizeUnitCmd::setJumpingPos() { _catalog->setLastModifiedPos(_pos); } //END LokalizeUnitCmd //BEGIN LokalizeTargetCmd LokalizeTargetCmd::LokalizeTargetCmd(Catalog *catalog, const DocPosition& pos, const QString& name = QString()) : LokalizeUnitCmd(catalog, pos, name) {} void LokalizeTargetCmd::redo() { LokalizeUnitCmd::redo(); _prevTargetPhase = setPhaseForPart(_catalog, _catalog->activePhase(), _pos, DocPosition::Target); } void LokalizeTargetCmd::undo() { LokalizeUnitCmd::undo(); setPhaseForPart(_catalog, _prevTargetPhase, _pos, DocPosition::Target); } //END LokalizeTargetCmd //BEGIN InsTextCmd InsTextCmd::InsTextCmd(Catalog *catalog, const DocPosition& pos, const QString& str) : LokalizeTargetCmd(catalog, pos, i18nc("@item Undo action item", "Insertion")) , _str(str) {} bool InsTextCmd::mergeWith(const QUndoCommand *other) { const DocPosition otherPos = static_cast(other)->pos(); if ((other->id() != id()) || (otherPos.entry != _pos.entry) || (otherPos.form != _pos.form) || (otherPos.offset != _pos.offset + _str.size()) ) return false; const QString& otherStr = static_cast(other)->_str; if (otherStr.isEmpty() || _str.isEmpty()) //just a precaution return false; //be close to behaviour of LibreOffice if (!_str.at(_str.size() - 1).isSpace() && otherStr.at(0).isSpace()) return false; _str += otherStr; return true; } void InsTextCmd::doRedo() { Catalog& catalog = *_catalog; DocPosition pos = _pos; pos.offset += _str.size(); catalog.setLastModifiedPos(pos); catalog.targetInsert(_pos, _str); } void InsTextCmd::doUndo() { _catalog->targetDelete(_pos, _str.size()); } //END InsTextCmd //BEGIN DelTextCmd DelTextCmd::DelTextCmd(Catalog *catalog, const DocPosition &pos, const QString &str) : LokalizeTargetCmd(catalog, pos, i18nc("@item Undo action item", "Deletion")) , _str(str) {} bool DelTextCmd::mergeWith(const QUndoCommand *other) { const DocPosition otherPos = static_cast(other)->pos(); if ( (other->id() != id()) || (otherPos.entry != _pos.entry) || (otherPos.form != _pos.form) ) return false; //Delete if (otherPos.offset == _pos.offset) { _str += static_cast(other)->_str; return true; } //BackSpace if (otherPos.offset == _pos.offset - static_cast(other)->_str.size()) { _str.prepend(static_cast(other)->_str); _pos.offset = otherPos.offset; return true; } return false; } void DelTextCmd::doRedo() { _catalog->targetDelete(_pos, _str.size()); } void DelTextCmd::doUndo() { //DocPosition pos=_pos; //pos.offset+=_str.size(); //_catalog.setLastModifiedPos(pos); _catalog->targetInsert(_pos, _str); } //END DelTextCmd //BEGIN SetStateCmd void SetStateCmd::push(Catalog *catalog, const DocPosition& pos, bool approved) { catalog->push(new SetStateCmd(catalog, pos, closestState(approved, catalog->activePhaseRole()))); } void SetStateCmd::instantiateAndPush(Catalog *catalog, const DocPosition& pos, TargetState state) { catalog->push(new SetStateCmd(catalog, pos, state)); } SetStateCmd::SetStateCmd(Catalog *catalog, const DocPosition& pos, TargetState state) : LokalizeUnitCmd(catalog, pos, i18nc("@item Undo action item", "Approvement toggling")) , _state(state) , _prevState(SignedOff) //shut up static analyzer {} void SetStateCmd::doRedo() { _prevState = _catalog->setState(_pos, _state); } void SetStateCmd::doUndo() { _catalog->setState(_pos, _prevState); } //END SetStateCmd //BEGIN InsTagCmd InsTagCmd::InsTagCmd(Catalog *catalog, const DocPosition& pos, const InlineTag& tag) : LokalizeTargetCmd(catalog, pos, i18nc("@item Undo action item", "Markup Insertion")) , _tag(tag) { _pos.offset = tag.start; } void InsTagCmd::doRedo() { Catalog& catalog = *_catalog; DocPosition pos = _pos; pos.offset++; //between paired tags or after single tag catalog.setLastModifiedPos(pos); catalog.targetInsertTag(_pos, _tag); } void InsTagCmd::doUndo() { _catalog->targetDeleteTag(_pos); } //END InsTagCmd //BEGIN DelTagCmd DelTagCmd::DelTagCmd(Catalog *catalog, const DocPosition& pos) : LokalizeTargetCmd(catalog, pos, i18nc("@item Undo action item", "Markup Deletion")) {} void DelTagCmd::doRedo() { _tag = _catalog->targetDeleteTag(_pos); qCDebug(LOKALIZE_LOG) << "tag properties:" << _tag.start << _tag.end; } void DelTagCmd::doUndo() { Catalog& catalog = *_catalog; DocPosition pos = _pos; pos.offset++; //between paired tags or after single tag catalog.setLastModifiedPos(pos); catalog.targetInsertTag(_pos, _tag); } //END DelTagCmd //BEGIN SetNoteCmd SetNoteCmd::SetNoteCmd(Catalog *catalog, const DocPosition& pos, const Note& note) : LokalizeUnitCmd(catalog, pos, i18nc("@item Undo action item", "Note setting")) , _note(note) { _pos.part = DocPosition::Comment; } static void setNote(Catalog& catalog, DocPosition& _pos, const Note& note, Note& resultNote) { resultNote = catalog.setNote(_pos, note); int size = catalog.notes(_pos).size(); if (_pos.form >= size) _pos.form = -1; #if 0 else if (_pos.form == -1) _pos.form = size - 1; #endif } void SetNoteCmd::doRedo() { setNote(*_catalog, _pos, _note, _prevNote); } void SetNoteCmd::doUndo() { Note tmp; setNote(*_catalog, _pos, _prevNote, tmp); } void SetNoteCmd::setJumpingPos() { DocPosition pos = _pos; pos.form = 0; _catalog->setLastModifiedPos(pos); } //END SetNoteCmd //BEGIN UpdatePhaseCmd UpdatePhaseCmd::UpdatePhaseCmd(Catalog *catalog, const Phase& phase) : QUndoCommand(i18nc("@item Undo action item", "Update/add workflow phase")) , _catalog(catalog) , _phase(phase) {} void UpdatePhaseCmd::redo() { _prevPhase = _catalog->updatePhase(_phase); } void UpdatePhaseCmd::undo() { _catalog->updatePhase(_prevPhase); } //END UpdatePhaseCmd //BEGIN SetEquivTransCmd SetEquivTransCmd::SetEquivTransCmd(Catalog *catalog, const DocPosition& pos, bool equivTrans) : LokalizeTargetCmd(catalog, pos, i18nc("@item Undo action item", "Translation Equivalence Setting")) , _equivTrans(equivTrans) {} void SetEquivTransCmd::doRedo() { _catalog->setEquivTrans(_pos, _equivTrans); } void SetEquivTransCmd::doUndo() { _catalog->setEquivTrans(_pos, !_equivTrans); } //END SetEquivTransCmd bool fillTagPlaces(QMap& tagPlaces, const CatalogString& catalogString, int start, int len ) { QString target = catalogString.string; if (len == -1) len = target.size(); int t = start; while ((t = target.indexOf(TAGRANGE_IMAGE_SYMBOL, t)) != -1 && t < (start + len)) tagPlaces[t++] = 0; int i = catalogString.tags.size(); while (--i >= 0) { //qCWarning(LOKALIZE_LOG)<::const_iterator it = tagPlaces.constBegin(); while (it != tagPlaces.constEnd() && it.value()) ++it; return it == tagPlaces.constEnd(); } bool removeTargetSubstring(Catalog* catalog, DocPosition pos, int delStart, int delLen) { CatalogString targetWithTags = catalog->targetWithTags(pos); QString target = targetWithTags.string; qCDebug(LOKALIZE_LOG) << "called with" << delStart << "delLen" << delLen << "target:" << target; if (delLen == -1) delLen = target.length() - delStart; bool doTags = catalog->capabilities()&Tags; QMap tagPlaces; if (target.isEmpty() || (doTags && !fillTagPlaces(tagPlaces, targetWithTags, delStart, delLen))) { qCWarning(LOKALIZE_LOG) << "error removing text" << target; return false; } catalog->beginMacro(i18nc("@item Undo action item", "Remove text with markup")); //all indexes are ok (or target is just plain text) //modified=true; //qCWarning(LOKALIZE_LOG)<<"all indexes are ok"; QMapIterator it(tagPlaces); it.toBack(); while (it.hasPrevious()) { it.previous(); if (it.value() != 1) continue; pos.offset = it.key(); DelTagCmd* cmd = new DelTagCmd(catalog, pos); catalog->push(cmd); delLen -= 1 + cmd->tag().isPaired(); QString tmp = catalog->targetWithTags(pos).string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, u'*'); qCDebug(LOKALIZE_LOG) << "\tdeleting at" << it.key() << "current string:" << tmp << "delLen" << delLen; } //charsRemoved-=lenDecrement; QString tmp = catalog->targetWithTags(pos).string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, u'*'); qCDebug(LOKALIZE_LOG) << "offset" << delStart << delLen << "current string:" << tmp; pos.offset = delStart; if (delLen) { QString rText = catalog->targetWithTags(pos).string.mid(delStart, delLen); rText.remove(TAGRANGE_IMAGE_SYMBOL); qCDebug(LOKALIZE_LOG) << "rText" << rText << "delStart" << delStart << rText.size(); if (!rText.isEmpty()) catalog->push(new DelTextCmd(catalog, pos, rText)); } tmp = catalog->targetWithTags(pos).string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, u'*'); qCDebug(LOKALIZE_LOG) << "current string:" << tmp; catalog->endMacro(); return true; } void insertCatalogString(Catalog* catalog, DocPosition pos, const CatalogString& catStr, int start) { QMap posToTag; int i = catStr.tags.size(); bool containsMarkup = i; while (--i >= 0) { const InlineTag& tag = catStr.tags.at(i); //qCWarning(LOKALIZE_LOG)<<"\t"<beginMacro(i18nc("@item Undo action item", "Insert text with markup")); i = 0; int prev = 0; while ((i = catStr.string.indexOf(TAGRANGE_IMAGE_SYMBOL, i)) != -1) { qCDebug(LOKALIZE_LOG) << "TAGRANGE_IMAGE_SYMBOL" << i; //text that was before tag we found if (i - prev) { pos.offset = start + prev; catalog->push(new InsTextCmd(catalog, pos, catStr.string.mid(prev, i - prev))); } //now dealing with tag qCDebug(LOKALIZE_LOG) << "posToTag.value(i)" << posToTag.value(i) << catStr.tags.size(); if (posToTag.value(i) < catStr.tags.size()) { InlineTag tag = catStr.tags.at(posToTag.value(i)); qCDebug(LOKALIZE_LOG) << i << "testing for tag" << tag.name() << tag.start << tag.start; if (tag.start == i) { //this is an opening tag (may be single tag) pos.offset = start + i; tag.start += start; tag.end += start; catalog->push(new InsTagCmd(catalog, pos, tag)); } } else { //HACK to keep positions in sync pos.offset = start + i; catalog->push(new InsTextCmd(catalog, pos, QStringLiteral(" "))); } prev = ++i; } pos.offset = start + prev; if (catStr.string.length() - prev > 0) catalog->push(new InsTextCmd(catalog, pos, catStr.string.mid(prev))); if (containsMarkup) catalog->endMacro(); } diff --git a/src/catalog/cmd.h b/src/catalog/cmd.h index 8c621a4..3a11722 100644 --- a/src/catalog/cmd.h +++ b/src/catalog/cmd.h @@ -1,249 +1,250 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef CMD_H #define CMD_H #include #include "pos.h" #include "note.h" #include "phase.h" #include "state.h" #include "catalogstring.h" class Catalog; enum Commands { Insert, Delete, InsertTag, DeleteTag, ToggleApprovement, EquivTrans, SetNote, UpdatePhase }; class LokalizeUnitCmd: public QUndoCommand { public: LokalizeUnitCmd(Catalog *catalog, const DocPosition& pos, const QString& name); ~LokalizeUnitCmd() override {} void undo() override; void redo() override; DocPosition pos()const { return _pos; } protected: virtual void doRedo() = 0; virtual void doUndo() = 0; /** * may be overridden to set customized pos * alternatively customized pos may be set manually in do*() */ virtual void setJumpingPos(); protected: Catalog* _catalog; DocPosition _pos; bool _firstModificationForThisEntry; // QString _prevPhase; currently xliffstorage doesn't support non-target phase setting }; class LokalizeTargetCmd: public LokalizeUnitCmd { public: LokalizeTargetCmd(Catalog *catalog, const DocPosition& pos, const QString& name); ~LokalizeTargetCmd() override {} void undo() override; void redo() override; protected: QString _prevTargetPhase; }; /** * how undo system works: * undo() and redo() functions call appropriate private method of Catalog to change catalog contents, * then set DocPosition (posBuffer var in Catalog), which is used to navigate editor to appr. place * @short Do insert text */ class InsTextCmd: public LokalizeTargetCmd { public: InsTextCmd(Catalog *catalog, const DocPosition& pos, const QString& str); ~InsTextCmd() override {} int id() const override { return Insert; } bool mergeWith(const QUndoCommand *other) override; void doRedo() override; void doUndo() override; private: QString _str; }; /// @see InsTextCmd class DelTextCmd: public LokalizeTargetCmd { public: DelTextCmd(Catalog *catalog, const DocPosition& pos, const QString& str); ~DelTextCmd() override {} int id() const override { return Delete; } bool mergeWith(const QUndoCommand *other) override; void doRedo() override; void doUndo() override; private: QString _str; }; class SetStateCmd: public LokalizeUnitCmd { private: SetStateCmd(Catalog *catalog, const DocPosition& pos, TargetState state); public: ~SetStateCmd() override {} int id() const override { return ToggleApprovement; } void doRedo() override; void doUndo() override; static void push(Catalog *catalog, const DocPosition& pos, bool approved); static void instantiateAndPush(Catalog *catalog, const DocPosition& pos, TargetState state); TargetState _state; TargetState _prevState; }; /// @short Do insert tag class InsTagCmd: public LokalizeTargetCmd { public: /// offset is taken from @a tag and not from @a pos InsTagCmd(Catalog *catalog, const DocPosition& pos, const InlineTag& tag); ~InsTagCmd() override {} int id() const override { return InsertTag; } void doRedo() override; void doUndo() override; private: InlineTag _tag; }; /** * TagRange is filled from document * * @short Do delete tag */ class DelTagCmd: public LokalizeTargetCmd { public: DelTagCmd(Catalog *catalog, const DocPosition& pos); ~DelTagCmd() override {} int id() const override { return DeleteTag; } void doRedo() override; void doUndo() override; InlineTag tag()const { return _tag; //used to get proprties of deleted tag } private: InlineTag _tag; }; /// @short Insert or remove (if content is empty) a note class SetNoteCmd: public LokalizeUnitCmd { public: /// @a pos.form is note number SetNoteCmd(Catalog *catalog, const DocPosition& pos, const Note& note); ~SetNoteCmd() override {} int id() const override { return SetNote; } protected: void doRedo() override; void doUndo() override; void setJumpingPos() override; private: Note _note; Note _prevNote; }; /// @short Add or remove (if content is empty) a phase class UpdatePhaseCmd: public QUndoCommand { public: /// @a pos.form is note number UpdatePhaseCmd(Catalog *catalog, const Phase& phase); ~UpdatePhaseCmd() override {} int id() const override { return UpdatePhase; } void redo() override; void undo() override; private: Catalog* _catalog; Phase _phase; Phase _prevPhase; }; class SetEquivTransCmd: public LokalizeTargetCmd { public: SetEquivTransCmd(Catalog *catalog, const DocPosition& pos, bool equivTrans); ~SetEquivTransCmd() override {} int id() const override { return EquivTrans; } void doRedo() override; void doUndo() override; private: bool _equivTrans; }; /** * CatalogString cmds helper function. * * tagPlaces: pos -> int: * >0 if both start and end parts of tag were (to be) deleted * 1 means this is start, 2 means this is end * @returns false if it can't find second part of any paired tag in the range */ bool fillTagPlaces(QMap& tagPlaces, const CatalogString& catalogString, int start, int len); bool removeTargetSubstring(Catalog* catalog, DocPosition pos, int delStart = 0, int delLen = -1); void insertCatalogString(Catalog* catalog, DocPosition pos, const CatalogString& catStr, int start = 0); #endif // CMD_H diff --git a/src/catalog/gettext/catalogitem.cpp b/src/catalog/gettext/catalogitem.cpp index 5008779..a3e104a 100644 --- a/src/catalog/gettext/catalogitem.cpp +++ b/src/catalog/gettext/catalogitem.cpp @@ -1,345 +1,346 @@ /* **************************************************************************** This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer 2002 by Stanislav Visnovsky Copyright (C) 2006 by Nicolas GOUTTE 2007-2012 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "catalogitem.h" #include "lokalize_debug.h" #include using namespace GettextCatalog; QString CatalogItem::msgctxt(const bool noNewlines) const { QString msgctxt = d._msgctxt; if (noNewlines) return msgctxt.replace(QLatin1Char('\n'), QLatin1Char(' ')); //" " or "" ? else return msgctxt; } const QString& CatalogItem::msgstr(const int form) const { if (Q_LIKELY(form < d._msgstrPlural.size())) return d._msgstrPlural.at(form); else return d._msgstrPlural.last(); } bool CatalogItem::prependEmptyForMsgid(const int form) const { Q_UNUSED(form) return d._prependMsgIdEmptyLine; } bool CatalogItem::prependEmptyForMsgstr(const int form) const { Q_UNUSED(form) return d._prependMsgStrEmptyLine; } const QVector& CatalogItem::msgstrPlural() const { return d._msgstrPlural; } const QVector& CatalogItem::msgidPlural() const { return d._msgidPlural; } QStringList CatalogItem::allPluralForms(CatalogItem::Part part, bool stripNewLines) const { QStringList result = (part == CatalogItem::Source ? d._msgidPlural : d._msgstrPlural).toList(); if (stripNewLines) { result.replaceInStrings(QStringLiteral("\n"), QString()); } return result; } void CatalogItem::setMsgctxt(const QString& msg) { d._msgctxt = msg; d._msgctxt.squeeze(); d._keepEmptyMsgCtxt = msg.isEmpty(); } void CatalogItem::setMsgid(const QString& msg, const int form) { if (form >= d._msgidPlural.size()) d._msgidPlural.resize(form + 1); d._msgidPlural[form] = msg; } void CatalogItem::setMsgid(const QStringList& msg) { d._msgidPlural = msg.toVector(); //TODO for (QVector::iterator it = d._msgidPlural.begin(); it != d._msgidPlural.end(); ++it) it->squeeze(); } void CatalogItem::setMsgid(const QStringList& msg, bool prependEmptyLine) { d._prependMsgIdEmptyLine = prependEmptyLine; d._msgidPlural = msg.toVector(); //TODO for (QVector::iterator it = d._msgidPlural.begin(); it != d._msgidPlural.end(); ++it) it->squeeze(); } void CatalogItem::setMsgid(const QVector& msg) { d._msgidPlural = msg; for (QVector::iterator it = d._msgidPlural.begin(); it != d._msgidPlural.end(); ++it) it->squeeze(); } void CatalogItem::setMsgstr(const QString& msg, const int form) { if (form >= d._msgstrPlural.size()) d._msgstrPlural.resize(form + 1); d._msgstrPlural[form] = msg; } void CatalogItem::setMsgstr(const QStringList& msg) { //TODO d._msgstrPlural = msg.toVector(); } void CatalogItem::setMsgstr(const QStringList& msg, bool prependEmptyLine) { d._prependMsgStrEmptyLine = prependEmptyLine; d._msgstrPlural = msg.toVector(); } void CatalogItem::setMsgstr(const QVector& msg) { d._msgstrPlural = msg; } void CatalogItem::setComment(const QString& com) { { //static QMutex reMutex; //QMutexLocker reLock(&reMutex); //avoid crash #281033 //now we have a bigger scale mutex in GettextStorage static QRegExp fuzzyRegExp(QStringLiteral("((?:^|\n)#(?:,[^,]*)*),\\s*fuzzy")); d._fuzzyCached = com.contains(fuzzyRegExp); } d._comment = com; d._comment.squeeze(); } bool CatalogItem::isUntranslated() const { return d.isUntranslated(); } bool CatalogItem::isUntranslated(uint form) const { return d.isUntranslated(form); } #if 0 QStringList CatalogItem::errors() const { return d._errors; } bool CatalogItem::isCformat() const { // Allow "possible-c-format" (from xgettext --debug) or "c-format" // Note the regexp (?: ) is similar to () but it does not capture (so it is faster) return d._comment.indexOf(QRegExp(",\\s*(?:possible-)c-format")) == -1; } bool CatalogItem::isNoCformat() const { return d._comment.indexOf(QRegExp(",\\s*no-c-format")) == -1; } bool CatalogItem::isQtformat() const { return d._comment.indexOf(QRegExp(",\\s*qt-format")) == -1; } bool CatalogItem::isNoQtformat() const { return d._comment.indexOf(QRegExp(",\\s*no-qt-format")) == -1; } bool CatalogItem::isUntranslated() const { return d._msgstr.first().isEmpty(); } int CatalogItem::totalLines() const { int lines = 0; if (!d._comment.isEmpty()) { lines = d._comment.count('\n') + 1; } int msgctxtLines = 0; if (!d._msgctxt.isEmpty()) { msgctxtLines = d._msgctxt.count('\n') + 1; } int msgidLines = 0; QStringList::ConstIterator it; for (it = d._msgid.begin(); it != d._msgid.end(); ++it) { msgidLines += (*it).count('\n') + 1; } int msgstrLines = 0; for (it = d._msgstr.begin(); it != d._msgstr.end(); ++it) { msgstrLines += (*it).count('\n') + 1; } if (msgctxtLines > 1) msgctxtLines++; if (msgidLines > 1) msgidLines++; if (msgstrLines > 1) msgstrLines++; lines += (msgctxtLines + msgidLines + msgstrLines); return lines; } void CatalogItem::setSyntaxError(bool on) { if (on && !d._errors.contains("syntax error")) d._errors.append("syntax error"); else d._errors.removeAll("syntax error"); } #endif QStringList CatalogItem::msgstrAsList() const { if (d._msgstrPlural.isEmpty()) { qCWarning(LOKALIZE_LOG) << "This should never happen!"; return QStringList(); } QStringList list(d._msgstrPlural.first().split('\n', QString::SkipEmptyParts)); if (d._msgstrPlural.first() == QLatin1String("\n")) list.prepend(QString()); if (list.isEmpty()) list.append(QString()); return list; } void CatalogItem::setFuzzy() { d._fuzzyCached = true; if (d._comment.isEmpty()) { d._comment = QStringLiteral("#, fuzzy"); return; } int p = d._comment.indexOf(QLatin1String("#,")); if (p != -1) { d._comment.replace(p, 2, QStringLiteral("#, fuzzy,")); return; } QString comment = d._comment; static QRegExp a("\\#\\:[^\n]*\n"); p = a.indexIn(comment); if (p != -1) { d._comment = comment.insert(p + a.matchedLength(), QLatin1String("#, fuzzy\n")); return; } p = d._comment.indexOf(QLatin1String("\n#|")); if (p != -1) { d._comment.insert(p, QLatin1String("\n#, fuzzy")); return; } if (d._comment.startsWith(QLatin1String("#|"))) { d._comment.prepend(QLatin1String("#, fuzzy\n")); return; } if (!(d._comment.endsWith(QLatin1Char('\n')))) d._comment += QLatin1Char('\n'); d._comment += QLatin1String("#, fuzzy"); } void CatalogItem::unsetFuzzy() { d._fuzzyCached = false; static const QRegExp rmFuzzyRe(QStringLiteral(",\\s*fuzzy")); d._comment.remove(rmFuzzyRe); // remove empty comment lines d._comment.remove(QRegExp(QStringLiteral("\n#\\s*$"))); d._comment.remove(QRegExp(QStringLiteral("^#\\s*$"))); d._comment.remove(QRegExp(QStringLiteral("#\\s*\n"))); d._comment.remove(QRegExp(QStringLiteral("^#\\s*\n"))); } #if 0 QString CatalogItem::nextError() const { return d._errors.first(); } void CatalogItem::clearErrors() { d._errors.clear(); } void CatalogItem::appendError(const QString& error) { if (!d._errors.contains(error)) d._errors.append(error); } void CatalogItem::removeError(const QString& error) { d._errors.removeAt(d._errors.indexOf(error)); } #endif // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/catalog/gettext/catalogitem.h b/src/catalog/gettext/catalogitem.h index aee69a4..cbd225b 100644 --- a/src/catalog/gettext/catalogitem.h +++ b/src/catalog/gettext/catalogitem.h @@ -1,181 +1,182 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer Copyright (C) 2002-2003 by Stanislav Visnovsky Copyright (C) 2006 by Nicolas GOUTTE Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef CATALOGITEM_H #define CATALOGITEM_H #include #include "catalogitem_private.h" namespace GettextCatalog { /** * This class represents an entry in a catalog. * It contains the comment, the Msgid and the Msgstr. * It defines some functions to query the state of the entry * (fuzzy, untranslated, cformat). * * @short Represents an entry in a Gettext catalog * @author Matthias Kiefer * @author Nick Shaforostoff */ class CatalogItem { public: explicit CatalogItem() {} CatalogItem(const CatalogItem& item): d(item.d) {} ~CatalogItem() {} bool isFuzzy() const { return d._fuzzyCached; //", fuzzy" in comment } bool isCformat() const; //", c-format" or possible-c-format in comment (from the debug parameter of xgettext) bool isNoCformat() const; //", no-c-format" in comment bool isQtformat() const; //", qt-format" in comment bool isNoQtformat() const; //", no-qt-format" in comment bool isUntranslated() const; bool isUntranslated(uint form) const; inline bool isPlural() const { return d._plural; } inline void setPlural(bool plural = true) { d._plural = plural; } void setSyntaxError(bool); /** returns the number of lines, the entry will need in a file */ int totalLines() const; /** cleares the item */ inline void clear() { d.clear(); } const QString& comment() const { return d._comment; } QString msgctxt(const bool noNewlines = false) const; const QString& msgid(const int form = 0) const { return d.msgid(form); } const QString& msgstr(const int form = 0) const; const QVector& msgstrPlural() const; const QVector& msgidPlural() const; enum Part {Source, Target}; QStringList allPluralForms(CatalogItem::Part, bool stripNewLines = false) const; bool prependEmptyForMsgid(const int form = 0) const; bool prependEmptyForMsgstr(const int form = 0) const; bool keepEmptyMsgCtxt() const { return d._keepEmptyMsgCtxt; } QStringList msgstrAsList() const; void setComment(const QString& com); void setMsgctxt(const QString& msg); void setMsgid(const QString& msg, const int form = 0); void setMsgid(const QStringList& msg); void setMsgid(const QStringList& msg, bool prependEmptyLine); void setMsgid(const QVector& msg); void setMsgstr(const QString& msg, const int form = 0); void setMsgstr(const QStringList& msg); void setMsgstr(const QStringList& msg, bool prependEmptyLine); void setMsgstr(const QVector& msg); void setValid(bool v) { d._valid = v; } bool isValid() const { return d._valid; } #if 0 /** * @return the list of all errors of this item */ QStringList errors() const; QString nextError() const; void clearErrors(); void removeError(const QString& error); void appendError(const QString& error); /** * makes some sanity checks and set status accordingly * @return the new status of this item * @see CatalogItem::Error * @param accelMarker a char, that marks the keyboard accelerators * @param contextInfo a regular expression, that determines what is * the context information * @param singularPlural a regular expression, that determines what is * string with singular and plural form * @param neededLines how many lines a string with singular-plural form * must have */ int checkErrors(QChar accelMarker, const QRegExp& contextInfo , const QRegExp& singularPlural, const int neededLines); #endif inline void operator=(const CatalogItem& rhs) { d.assign(rhs.d); } private: CatalogItemPrivate d; friend class GettextStorage; void setFuzzy(); void unsetFuzzy(); }; } #endif // CATALOGITEM_H diff --git a/src/catalog/gettext/catalogitem_private.h b/src/catalog/gettext/catalogitem_private.h index 82d63d6..bfef428 100644 --- a/src/catalog/gettext/catalogitem_private.h +++ b/src/catalog/gettext/catalogitem_private.h @@ -1,147 +1,148 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer 2002 by Stanislav Visnovsky 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef CATALOGITEMPRIVATE_H #define CATALOGITEMPRIVATE_H #include #include #include namespace GettextCatalog { /** * This class represents data for an entry in a catalog. * It contains the comment, the Msgid and the Msgstr. * It defines some functions to query the state of the entry * (fuzzy, untranslated, cformat). * * @short Class, representing an entry in a catalog * @author Matthias Kiefer * @author Stanislav Visnovsky * @author Nick Shaforostoff */ class CatalogItemPrivate { public: bool _plural; bool _valid; bool _fuzzyCached; bool _prependMsgIdEmptyLine; bool _prependMsgStrEmptyLine; bool _keepEmptyMsgCtxt; QString _comment; QString _msgctxt; QVector _msgidPlural; QVector _msgstrPlural; //QVector _errors; CatalogItemPrivate() : _plural(false) , _valid(true) , _fuzzyCached(false) , _prependMsgIdEmptyLine(false) , _prependMsgStrEmptyLine(false) , _keepEmptyMsgCtxt(false) {} void clear(); void assign(const CatalogItemPrivate& other); bool isUntranslated() const; bool isUntranslated(uint form) const; const QString& msgid(const int form) const; }; inline void CatalogItemPrivate::clear() { _plural = false; _valid = true; _comment.clear(); _msgctxt.clear(); _msgidPlural.clear(); _msgstrPlural.clear(); //_errors.clear(); } inline void CatalogItemPrivate::assign(const CatalogItemPrivate& other) { _comment = other._comment; _msgctxt = other._msgctxt; _msgidPlural = other._msgidPlural; _msgstrPlural = other._msgstrPlural; _valid = other._valid; //_errors=other._errors; _plural = other._plural; _fuzzyCached = other._fuzzyCached; } inline bool CatalogItemPrivate::isUntranslated() const { int i = _msgstrPlural.size(); while (--i >= 0) if (_msgstrPlural.at(i).isEmpty()) return true; return false; } inline bool CatalogItemPrivate::isUntranslated(uint form) const { if ((int)form < _msgstrPlural.size()) return _msgstrPlural.at(form).isEmpty(); else return true; } inline const QString& CatalogItemPrivate::msgid(const int form) const { //if original lang is english, we have only 2 formz return (form < _msgidPlural.size()) ? _msgidPlural.at(form) : _msgidPlural.last(); } } #endif // CATALOGITEMPRIVATE_H diff --git a/src/catalog/gettext/exportplugin.cpp b/src/catalog/gettext/exportplugin.cpp index a0882ee..7a218fa 100644 --- a/src/catalog/gettext/exportplugin.cpp +++ b/src/catalog/gettext/exportplugin.cpp @@ -1,47 +1,48 @@ /* **************************************************************************** This file is part of KAider This file is based on the one from KBabel Copyright (C) 2002-2003 by Stanislav Visnovsky 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "catalogfileplugin.h" using namespace GettextCatalog; CatalogExportPlugin::CatalogExportPlugin() // : d(new CatalogExportPluginPrivate) { } CatalogExportPlugin::~CatalogExportPlugin() { // delete d; } diff --git a/src/catalog/gettext/gettextimport.cpp b/src/catalog/gettext/gettextimport.cpp index fb6bc04..6f1e13f 100644 --- a/src/catalog/gettext/gettextimport.cpp +++ b/src/catalog/gettext/gettextimport.cpp @@ -1,704 +1,705 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 1999-2000 by Matthias Kiefer 2001-2003 by Stanislav Visnovsky 2006 by Nicolas GOUTTE 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "gettextimport.h" #include "lokalize_debug.h" //#include #include #include #include #include #include #include #include #include #include "catalogitem.h" using namespace GettextCatalog; // GettextImportPlugin::GettextImportPlugin(ExtraDataSaver* extraDataSaver) // : CatalogImportPlugin() // , _extraDataSaver(extraDataSaver) GettextImportPlugin::GettextImportPlugin() : CatalogImportPlugin() , _msgidMultiline(false) , _msgstrMultiline(false) , _gettextPluralForm(false) , _testBorked(false) , _obsolete(false) , _msgctxtPresent(false) , _rxMsgCtxt(QStringLiteral("^msgctxt\\s*\".*\"$")) , _rxMsgId(QStringLiteral("^msgid\\s*\".*\"$")) , _rxMsgIdPlural(QStringLiteral("^msgid_plural\\s*\".*\"$")) , _rxMsgIdPluralBorked(QStringLiteral("^msgid_plural\\s*\"?.*\"?$")) , _rxMsgIdBorked(QStringLiteral("^msgid\\s*\"?.*\"?$")) , _rxMsgIdRemQuotes(QStringLiteral("^msgid\\s*\"")) , _rxMsgLineRemEndQuote(QStringLiteral("\"$")) , _rxMsgLineRemStartQuote(QStringLiteral("^\"")) , _rxMsgLine(QStringLiteral("^\".*\\n?\"$")) , _rxMsgLineBorked(QStringLiteral("^\"?.+\\n?\"?$")) , _rxMsgStr(QStringLiteral("^msgstr\\s*\".*\\n?\"$")) , _rxMsgStrOther(QStringLiteral("^msgstr\\s*\"?.*\\n?\"?$")) , _rxMsgStrPluralStart(QStringLiteral("^msgstr\\[0\\]\\s*\".*\\n?\"$")) , _rxMsgStrPluralStartBorked(QStringLiteral("^msgstr\\[0\\]\\s*\"?.*\\n?\"?$")) , _rxMsgStrPlural(QStringLiteral("^msgstr\\[[0-9]+\\]\\s*\".*\\n?\"$")) , _rxMsgStrPluralBorked(QStringLiteral("^msgstr\\[[0-9]\\]\\s*\"?.*\\n?\"?$")) , _rxMsgStrRemQuotes(QStringLiteral("^msgstr\\s*\"?")) // , _rxMsgId (QStringLiteral("^msgid\\s*\"?.*\"?$")) , _obsoleteStart(QStringLiteral("#~")) , _msgctxtStart(QStringLiteral("msgctxt")) { } ConversionStatus GettextImportPlugin::load(QIODevice* device) { _testBorked = false; _errorLine = 0; // find codec for file // bool hadCodec; QTextCodec* codec = codecForDevice(device/*, &hadCodec*/); QTextStream stream(device); stream.seek(0); stream.setCodec(codec); //QIODevice *dev = stream.device(); //int fileSize = dev->size(); // if somethings goes wrong with the parsing, we don't have deleted the old contents CatalogItem tempHeader; //qCDebug(LOKALIZE_LOG) << "start parsing..."; QTime aaa; aaa.start(); // first read header const ConversionStatus status = readEntry(stream); bool recoveredErrorInHeader = false; if (Q_UNLIKELY(status == RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "Recovered error in header entry"; recoveredErrorInHeader = true; } else if (Q_UNLIKELY(status != OK)) { qCWarning(LOKALIZE_LOG) << "Parse error in header entry"; return status; } bool reconstructedHeader = !_msgid.isEmpty() && !_msgid.first().isEmpty(); //qCWarning(LOKALIZE_LOG) << "HEADER MSGID: " << _msgid; //qCWarning(LOKALIZE_LOG) << "HEADER MSGSTR: " << _msgstr; if (Q_UNLIKELY(reconstructedHeader)) { // The header must have an empty msgid qCWarning(LOKALIZE_LOG) << "Header entry has non-empty msgid. Creating a temporary header! " << _msgid; tempHeader.setMsgid(QString()); QString tmp( "Content-Type: text/plain; charset=UTF-8\\n" // Unknown charset "Content-Transfer-Encoding: 8bit\\n" "Mime-Version: 1.0"); tempHeader.setMsgstr(tmp); // We keep the comment of the first entry, as it might really be a header comment (at least partially) const QString comment("# Header entry was created by Lokalize.\n#\n" + _comment); tempHeader.setComment(comment); recoveredErrorInHeader = true; } else { tempHeader.setMsgid(_msgid); tempHeader.setMsgstr(_msgstr); tempHeader.setComment(_comment); } // if(tempHeader.isFuzzy()) // { // tempHeader.removeFuzzy(); // } // check if header seems to indicate docbook content generated by xml2pot const bool docbookContent = tempHeader.msgstr().contains("application/x-xml2pot"); // now parse the rest of the file uint counter = 0; QList errorIndex; //bool recoveredError=false; bool docbookFile = false; ExtraDataSaver _extraDataSaver; ConversionStatus success = OK; while (!stream.atEnd()) { if (reconstructedHeader) reconstructedHeader = false; else success = readEntry(stream); if (Q_LIKELY(success == OK)) { if (_obsolete) _extraDataSaver(_comment); else { CatalogItem tempCatItem; tempCatItem.setPlural(_gettextPluralForm); tempCatItem.setMsgid(_msgid, _msgidMultiline); tempCatItem.setMsgstr(_msgstr, _msgstrMultiline); if (_msgctxtPresent) tempCatItem.setMsgctxt(_msgctxt); tempCatItem.setComment(_comment); // add new entry to the list of entries appendCatalogItem(tempCatItem); // check if first comment seems to indicate a docbook source file if (counter == 0) docbookFile = tempCatItem.comment().contains(".docbook"); } } else if (Q_UNLIKELY(success == RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "Recovered parse error in entry: " << counter; //recoveredError=true; errorIndex.append(counter); CatalogItem tempCatItem; tempCatItem.setPlural(_gettextPluralForm); tempCatItem.setMsgid(_msgid); tempCatItem.setMsgstr(_msgstr); if (_msgctxtPresent) tempCatItem.setMsgctxt(_msgctxt); tempCatItem.setComment(_comment); // add new entry to the list of entries appendCatalogItem(tempCatItem); } else if (success == PARSE_ERROR) { qCDebug(LOKALIZE_LOG) << "Parse error in entry: " << counter; return PARSE_ERROR; } else { qCDebug(LOKALIZE_LOG) << "Unknown success status, assumig parse error " << success; return PARSE_ERROR; } counter++; } // TODO: can we check that there is no useful entry? if (Q_UNLIKELY(!counter && !recoveredErrorInHeader)) { // Empty file? (Otherwise, there would be a try of getting an entry and the count would be 1 !) qCDebug(LOKALIZE_LOG) << " Empty file?"; return PARSE_ERROR; } //qCDebug(LOKALIZE_LOG) << " ready"; // We have successfully loaded the file (perhaps with recovered errors) // qCWarning(LOKALIZE_LOG) << " done in " << aaa.elapsed() <<_extraDataSaver->extraData.size() << endl; setGeneratedFromDocbook(docbookContent || docbookFile); setHeader(tempHeader); setCatalogExtraData(_extraDataSaver.extraData); setErrorIndex(errorIndex); setCodec(codec); //setMimeTypes( "text/x-gettext-translation" ); #if 0 if (Q_UNLIKELY(recoveredErrorInHeader)) { qCDebug(LOKALIZE_LOG) << " Returning: header error"; return RECOVERED_HEADER_ERROR; } else if (Q_UNLIKELY(recoveredError)) { qCDebug(LOKALIZE_LOG) << " Returning: recovered parse error"; return RECOVERED_PARSE_ERROR; } else #endif { //qCDebug(LOKALIZE_LOG) << " Returning: OK! :-)"; return OK; } } QTextCodec* GettextImportPlugin::codecForDevice(QIODevice* device/*, bool* hadCodec*/) { QTextStream stream(device); stream.seek(0); _errorLine = 0; stream.setCodec("UTF-8"); stream.setAutoDetectUnicode(true); //this way we can QTextCodec* codec = stream.codec(); //detect UTF-16 ConversionStatus status = readEntry(stream); if (Q_UNLIKELY(status != OK && status != RECOVERED_PARSE_ERROR)) { qCDebug(LOKALIZE_LOG) << "wasn't able to read header"; return codec; } QRegExp regexp(QStringLiteral("Content-Type:\\s*\\w+/[-\\w]+;?\\s*charset\\s*=\\s*(\\S+)\\s*\\\\n")); if (regexp.indexIn(_msgstr.first()) == -1) { qCDebug(LOKALIZE_LOG) << "no charset entry found"; return codec; } const QString charset = regexp.cap(1); if (charset != QLatin1String("UTF-8")) qCDebug(LOKALIZE_LOG) << "charset:" << charset; if (charset.isEmpty()) { qCWarning(LOKALIZE_LOG) << "No charset defined! Assuming UTF-8!"; return codec; } // "CHARSET" is the default charset entry in a template (pot). // characters in a template should be either pure ascii or // at least utf8, so utf8-codec can be used for both. if (charset.contains(QLatin1String("CHARSET"))) { qCDebug(LOKALIZE_LOG) << QString("file seems to be a template: using utf-8 encoding."); return QTextCodec::codecForName("utf8");; } QTextCodec* t = 0; t = QTextCodec::codecForName(charset.toLatin1()); if (t) return t; else qCWarning(LOKALIZE_LOG) << "charset found, but no codec available, using UTF-8 instead"; return codec;//UTF-8 } ConversionStatus GettextImportPlugin::readEntry(QTextStream& stream) { ConversionStatus result = readEntryRaw(stream); const QString FROM = QStringLiteral("\\\""); const QString TO = QStringLiteral("\""); _msgstr.replaceInStrings(FROM, TO); _msgid.replaceInStrings(FROM, TO); _msgctxt.replace(FROM, TO); return result; } ConversionStatus GettextImportPlugin::readEntryRaw(QTextStream& stream) { //qCDebug(LOKALIZE_LOG) << " START"; enum {Begin, Comment, Msgctxt, Msgid, Msgstr} part = Begin; _trailingNewLines = 0; bool error = false; bool recoverableError = false; //bool seenMsgctxt=false; _msgstr.clear(); _msgstr.append(QString()); _msgid.clear(); _msgid.append(QString()); _msgctxt.clear(); _msgctxtPresent = false; _comment.clear(); _gettextPluralForm = false; _obsolete = false; QStringList::Iterator msgstrIt = _msgstr.begin(); QString line; while (!stream.atEnd()) { _errorLine++; //line=stream.readLine(); if (!_bufferedLine.isEmpty()) { line = _bufferedLine; _bufferedLine.clear(); } else line = stream.readLine(); static const QString lesslessless = QStringLiteral("<<<<<<<"); static const QString isisis = QStringLiteral("======="); static const QString moremoremore = QStringLiteral(">>>>>>>"); if (Q_UNLIKELY(line.startsWith(lesslessless) || line.startsWith(isisis) || line.startsWith(moremoremore))) { // We have found a CVS/SVN conflict marker. Abort. // (It cannot be any useful data of the PO file, as otherwise the line would start with at least a quote) qCWarning(LOKALIZE_LOG) << "CVS/SVN conflict marker found! Aborting!" << endl << line << endl; return PARSE_ERROR; } // remove whitespaces from beginning and end of line line = line.trimmed(); // remember wrapping state to save file nicely int len = line.length(); if (len) { _trailingNewLines = 0; if (_maxLineLength < len && line.at(0) != '#') _maxLineLength = len; } else ++_trailingNewLines; if (part == Begin) { // ignore trailing newlines if (!len) continue; if (line.startsWith(_obsoleteStart)) { _obsolete = true; part = Comment; _comment = line; } else if (line.startsWith('#')) { part = Comment; _comment = line; } else if (line.startsWith(_msgctxtStart) && line.contains(_rxMsgCtxt)) { part = Msgctxt; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgctxt\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgctxt = line; _msgctxtPresent = true; //seenMsgctxt=true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no comment, msgctxt or msgid found after a comment: " << line; error = true; break; } } else if (part == Comment) { if (!len && _obsolete) return OK; if (!len) continue; else if (line.startsWith(_obsoleteStart)) { _comment += ('\n' + line); _obsolete = true; } else if (line.startsWith('#')) { _comment += ('\n' + line); } else if (line.startsWith(_msgctxtStart) && line.contains(_rxMsgCtxt)) { part = Msgctxt; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgctxt\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgctxt = line; _msgctxtPresent = true; //seenMsgctxt=true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp("^msgid\\s*\"?")); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no comment or msgid found after a comment while parsing: " << _comment; error = true; break; } } else if (part == Msgctxt) { if (!len) continue; else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); // add Msgctxt line to item if (_msgctxt.isEmpty()) _msgctxt = line; else _msgctxt += ('\n' + line); _msgctxtPresent = true; } else if (line.contains(_rxMsgId)) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(_rxMsgIdRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdBorked))) { part = Msgid; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgidMultiline = line.isEmpty(); (*(_msgid).begin()) = line; if (!line.isEmpty()) recoverableError = true; } else { qCDebug(LOKALIZE_LOG) << "no msgid found after a msgctxt while parsing: " << _msgctxt; error = true; break; } } else if (part == Msgid) { if (!len) continue; else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); QStringList::Iterator it; if (_gettextPluralForm) { it = _msgid.end(); --it; } else it = _msgid.begin(); // add Msgid line to item if (it->isEmpty()) (*it) = line; else (*it) += ('\n' + line); } else if (line.contains(_rxMsgIdPlural)) { part = Msgid; _gettextPluralForm = true; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid_plural\\s*\""))); line.remove(_rxMsgLineRemEndQuote); _msgid.append(line); } // one of the quotation marks is missing else if (Q_UNLIKELY(/*_testBorked&&*/ line.contains(_rxMsgIdPluralBorked))) { part = Msgid; _gettextPluralForm = true; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgid_plural\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgid.append(line); if (!line.isEmpty()) recoverableError = true; } else if (!_gettextPluralForm && (line.contains(_rxMsgStr))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(_rxMsgStrRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; } else if (!_gettextPluralForm && (line.contains(_rxMsgStrOther))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(_rxMsgStrRemQuotes); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; if (!line.isEmpty()) recoverableError = true; } else if (_gettextPluralForm && (line.contains(_rxMsgStrPluralStart))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[0\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; } else if (Q_UNLIKELY(/*_testBorked&&*/ _gettextPluralForm && line.contains(_rxMsgStrPluralStartBorked))) { part = Msgstr; // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[0\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstrMultiline = line.isEmpty(); (*msgstrIt) = line; if (!line.isEmpty()) recoverableError = true; } else if (line.startsWith('#')) { // ### TODO: could this be considered recoverable? qCDebug(LOKALIZE_LOG) << "comment found after a msgid while parsing: " << _msgid.first(); error = true; break; } else if (line.startsWith(QStringLiteral("msgid"))) { qCDebug(LOKALIZE_LOG) << "Another msgid found after a msgid while parsing: " << _msgid.first(); error = true; break; } // a line of the msgid with a missing quotation mark else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgLineBorked))) { recoverableError = true; // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); QStringList::Iterator it; if (_gettextPluralForm) { it = _msgid.end(); --it; } else it = _msgid.begin(); // add Msgid line to item if (it->isEmpty()) (*it) = line; else (*it) += ('\n' + line); } else { qCDebug(LOKALIZE_LOG) << "no msgstr found after a msgid while parsing: " << _msgid.first(); error = true; break; } } else if (part == Msgstr) { if (!len) break; // another line of the msgstr else if (line.contains(_rxMsgLine)) { // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); if (!(*msgstrIt).isEmpty()) (*msgstrIt) += '\n'; (*msgstrIt) += line; } else if (_gettextPluralForm && (line.contains(_rxMsgStrPlural))) { // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[[0-9]+\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstr.append(line); msgstrIt = _msgstr.end(); --msgstrIt; } else if (line.startsWith('#') || line.startsWith(QStringLiteral("msgid"))) { _errorLine--; _bufferedLine = line; break; } else if (Q_UNLIKELY(/*_testBorked&&*/ _gettextPluralForm && (line.contains(_rxMsgStrPluralBorked)))) { // remove quotes at beginning and the end of the lines line.remove(QRegExp(QStringLiteral("^msgstr\\[[0-9]\\]\\s*\"?"))); line.remove(_rxMsgLineRemEndQuote); _msgstr.append(line); msgstrIt = _msgstr.end(); --msgstrIt; if (!line.isEmpty()) recoverableError = true; } else if (line.startsWith(QLatin1String("msgstr"))) { qCDebug(LOKALIZE_LOG) << "Another msgstr found after a msgstr while parsing: " << line << _msgstr.last(); error = true; break; } // another line of the msgstr with a missing quotation mark else if (Q_UNLIKELY(/*_testBorked&&*/line.contains(_rxMsgLineBorked))) { recoverableError = true; // remove quotes at beginning and the end of the lines line.remove(_rxMsgLineRemStartQuote); line.remove(_rxMsgLineRemEndQuote); if (!(*msgstrIt).isEmpty()) (*msgstrIt) += '\n'; (*msgstrIt) += line; } else { qCDebug(LOKALIZE_LOG) << "no msgid or comment found after a msgstr while parsing: " << _msgstr.last(); error = true; break; } } } /* if(_gettextPluralForm) { qCDebug(LOKALIZE_LOG) << "gettext plural form:\n" << "msgid:\n" << _msgid.first() << "\n" << "msgid_plural:\n" << _msgid.last() << "\n" << endl; int counter=0; for(QStringList::Iterator it = _msgstr.begin(); it != _msgstr.end(); ++it) { qCDebug(LOKALIZE_LOG) << "msgstr[" << counter << "]:\n" << (*it) << endl; counter++; } } */ //qCDebug(LOKALIZE_LOG) << " NEAR RETURN"; if (Q_UNLIKELY(error)) return PARSE_ERROR; else if (Q_UNLIKELY(recoverableError)) return RECOVERED_PARSE_ERROR; else return OK; } // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/src/catalog/gettext/gettextstorage.cpp b/src/catalog/gettext/gettextstorage.cpp index 3647f83..f5382a4 100644 --- a/src/catalog/gettext/gettextstorage.cpp +++ b/src/catalog/gettext/gettextstorage.cpp @@ -1,494 +1,495 @@ /* Copyright 2008-2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "gettextstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "catalogitem_private.h" #include "gettextimport.h" #include "gettextexport.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include "diff.h" #include #include #include #include #include #include QMutex regExMutex; // static QString GNUPluralForms(const QString& lang); using namespace GettextCatalog; GettextStorage::GettextStorage() : CatalogStorage() , m_codec(0) , m_maxLineLength(80) , m_trailingNewLines(0) , m_generatedFromDocbook(false) { } GettextStorage::~GettextStorage() { } //BEGIN OPEN/SAVE int GettextStorage::load(QIODevice* device/*, bool readonly*/) { //GettextImportPlugin importer=GettextImportPlugin(readonly?(new ExtraDataSaver()):(new ExtraDataListSaver())); GettextImportPlugin importer; ConversionStatus status = OK; int errorLine; { QMutexLocker locker(®ExMutex); status = importer.open(device, this, &errorLine); } //for langs with more than 2 forms //we create any form-entries additionally needed uint i = 0; uint lim = size(); while (i < lim) { CatalogItem& item = m_entries[i]; if (item.isPlural() && item.msgstrPlural().count() < m_numberOfPluralForms ) { QVector msgstr(item.msgstrPlural()); while (msgstr.count() < m_numberOfPluralForms) msgstr.append(QString()); item.setMsgstr(msgstr); } ++i; } //qCompress(m_storage->m_catalogExtraData.join("\n\n").toUtf8(),9); return status == OK ? 0 : (errorLine + 1); } bool GettextStorage::save(QIODevice* device, bool belongsToProject) { QString header = m_header.msgstr(); QString comment = m_header.comment(); QString catalogProjectId;//=m_url.fileName(); //catalogProjectId=catalogProjectId.left(catalogProjectId.lastIndexOf('.')); { QMutexLocker locker(®ExMutex); updateHeader(header, comment, m_targetLangCode, m_numberOfPluralForms, catalogProjectId, m_generatedFromDocbook, belongsToProject, /*forSaving*/true, m_codec); } m_header.setMsgstr(header); m_header.setComment(comment); //GettextExportPlugin exporter(m_maxLineLength>70?m_maxLineLength:-1, m_trailingNewLines);// this is kinda hackish... GettextExportPlugin exporter(Project::instance()->wordWrap(), m_trailingNewLines); ConversionStatus status = OK; status = exporter.save(device/*x-gettext-translation*/, this, m_codec); return status == OK; } //END OPEN/SAVE //BEGIN STORAGE TRANSLATION int GettextStorage::size() const { return m_entries.size(); } static const QChar altSep(156); static InlineTag makeInlineTag(int i) { static const QString altSepText(QStringLiteral(" | ")); static const QString ctype = i18n("separator for different-length string alternatives"); return InlineTag(i, i, InlineTag::x, QString::number(i), QString(), altSepText, ctype); } static CatalogString makeCatalogString(const QString& string) { CatalogString result; result.string = string; int i = 0; while ((i = result.string.indexOf(altSep, i)) != -1) { result.string[i] = TAGRANGE_IMAGE_SYMBOL; result.tags.append(makeInlineTag(i)); ++i; } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString GettextStorage::sourceWithTags(DocPosition pos) const { return makeCatalogString(source(pos)); } CatalogString GettextStorage::targetWithTags(DocPosition pos) const { return makeCatalogString(target(pos)); } QString GettextStorage::source(const DocPosition& pos) const { return m_entries.at(pos.entry).msgid(pos.form); } QString GettextStorage::target(const DocPosition& pos) const { return m_entries.at(pos.entry).msgstr(pos.form); } QString GettextStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (m_entries.at(pos.entry).isPlural()) { const QVector plurals = m_entries.at(pos.entry).msgidPlural(); QString pluralString; for (int i = 0; i < plurals.size(); i++) { QString str = plurals.at(i); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } pluralString += str; if (i != plurals.size() - 1) { pluralString += '|'; } } return pluralString; } else { QString str = m_entries.at(pos.entry).msgid(pos.form); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } } QString GettextStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { if (m_entries.at(pos.entry).isPlural()) { const QVector plurals = m_entries.at(pos.entry).msgstrPlural(); QString pluralString; for (int i = 0; i < plurals.size(); i++) { QString str = plurals.at(i); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } pluralString += str; if (i != plurals.size() - 1) { pluralString += '|'; } } return pluralString; } else { QString str = m_entries.at(pos.entry).msgstr(pos.form); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } } void GettextStorage::targetDelete(const DocPosition& pos, int count) { m_entries[pos.entry].d._msgstrPlural[pos.form].remove(pos.offset, count); } void GettextStorage::targetInsert(const DocPosition& pos, const QString& arg) { m_entries[pos.entry].d._msgstrPlural[pos.form].insert(pos.offset, arg); } void GettextStorage::setTarget(const DocPosition& pos, const QString& arg) { m_entries[pos.entry].d._msgstrPlural[pos.form] = arg; } void GettextStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { Q_UNUSED(tag); targetInsert(pos, altSep); } InlineTag GettextStorage::targetDeleteTag(const DocPosition& pos) { targetDelete(pos, 1); return makeInlineTag(pos.offset); } QStringList GettextStorage::sourceAllForms(const DocPosition& pos, bool stripNewLines) const { return m_entries.at(pos.entry).allPluralForms(CatalogItem::Source, stripNewLines); } QStringList GettextStorage::targetAllForms(const DocPosition& pos, bool stripNewLines) const { return m_entries.at(pos.entry).allPluralForms(CatalogItem::Target, stripNewLines); } QVector GettextStorage::altTrans(const DocPosition& pos) const { static const QRegExp alt_trans_mark_re(QStringLiteral("^#\\|")); QStringList prev = m_entries.at(pos.entry).comment().split('\n').filter(alt_trans_mark_re); QString oldSingular; QString oldPlural; QString* cur = &oldSingular; QStringList::iterator it = prev.begin(); static const QString msgid_plural_alt = QStringLiteral("#| msgid_plural \""); while (it != prev.end()) { if (it->startsWith(msgid_plural_alt)) cur = &oldPlural; int start = it->indexOf('\"') + 1; int end = it->lastIndexOf('\"'); if (start && end != -1) { if (!cur->isEmpty()) (*cur) += '\n'; if (!(cur->isEmpty() && (end - start) == 0)) //for multiline msgs (*cur) += it->midRef(start, end - start); } ++it; } if (pos.form == 0) cur = &oldSingular; cur->replace(QStringLiteral("\\\""), QStringLiteral("\"")); QVector result; if (!cur->isEmpty()) result << AltTrans(CatalogString(*cur), i18n("Previous source value, saved by Gettext during transition to a newer POT template")); return result; } Note GettextStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)<<"s"< l = notes(pos); if (l.size()) oldNote = l.first(); QStringList comment = m_entries.at(pos.entry).comment().split('\n'); //remove previous comment; QStringList::iterator it = comment.begin(); while (it != comment.end()) { if (it->startsWith(QLatin1String("# "))) it = comment.erase(it); else ++it; } if (note.content.size()) comment.prepend(QStringLiteral("# ") + note.content.split('\n').join(QStringLiteral("\n# "))); m_entries[pos.entry].setComment(comment.join(QStringLiteral("\n"))); //qCWarning(LOKALIZE_LOG)<<"e"< GettextStorage::notes(const DocPosition& docPosition, const QRegExp& re, int preLen) const { QVector result; QString content; QStringList note = m_entries.at(docPosition.entry).comment().split('\n').filter(re); foreach (const QString &s, note) { if (s.size() >= preLen) { content += s.midRef(preLen); content += QLatin1Char('\n'); } } if (!content.isEmpty()) { content.chop(1); result << Note(content); } return result; //i18nc("@info PO comment parsing. contains filename","Place:"); //i18nc("@info PO comment parsing","GUI place:"); } QVector GettextStorage::notes(const DocPosition& docPosition) const { static const QRegExp nre(QStringLiteral("^# ")); return notes(docPosition, nre, 2); } QVector GettextStorage::developerNotes(const DocPosition& docPosition) const { static const QRegExp dnre(QStringLiteral("^#\\. (?!i18n: file:)")); return notes(docPosition, dnre, 3); } QStringList GettextStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QStringList commentLines = m_entries.at(pos.entry).comment().split('\n'); static const QRegExp i18n_file_re(QStringLiteral("^#. i18n: file: ")); foreach (const QString &uiLine, commentLines.filter(i18n_file_re)) { foreach (const QStringRef &fileRef, uiLine.midRef(15).split(' ')) { result << fileRef.toString(); } } bool hasUi = !result.isEmpty(); static const QRegExp cpp_re(QStringLiteral("^#: ")); foreach (const QString &cppLine, commentLines.filter(cpp_re)) { if (hasUi && cppLine.startsWith(QLatin1String("#: rc.cpp"))) continue; foreach (const QStringRef &fileRef, cppLine.midRef(3).split(' ')) { result << fileRef.toString(); } } return result; } QStringList GettextStorage::context(const DocPosition& pos) const { return matchData(pos); } QStringList GettextStorage::matchData(const DocPosition& pos) const { QString ctxt = m_entries.at(pos.entry).msgctxt(); //KDE-specific //Splits @info:whatsthis and actual note /* if (ctxt.startsWith('@') && ctxt.contains(' ')) { QStringList result(ctxt.section(' ',0,0,QString::SectionSkipEmpty)); result<poDir()); updateHeader(values, comment, m_targetLangCode, m_numberOfPluralForms, catalogProjectId, m_generatedFromDocbook, belongsToProject, /*forSaving*/true, m_codec); m_header = newHeader; m_header.setComment(comment); m_header.setMsgstr(values); // setClean(false); //emit signalHeaderChanged(); return true; } qCWarning(LOKALIZE_LOG) << "header Not valid"; return false; } diff --git a/src/catalog/gettext/gettextstorage.h b/src/catalog/gettext/gettextstorage.h index ef01be6..47d0105 100644 --- a/src/catalog/gettext/gettextstorage.h +++ b/src/catalog/gettext/gettextstorage.h @@ -1,137 +1,138 @@ /* Copyright 2008-2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef GETTEXTSTORAGE_H #define GETTEXTSTORAGE_H #include #include #include "catalogitem.h" #include "catalogstorage.h" /** * Implementation of Gettext PO format support */ namespace GettextCatalog { /** * @short Implementation of storage for Gettext PO * @author Nick Shaforostoff */ class GettextStorage: public CatalogStorage { public: GettextStorage(); ~GettextStorage() override; int capabilities() const override { return 0; } int load(QIODevice* device/*, bool readonly=false*/) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; //flat-model interface (ignores XLIFF grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override { return pos.part == DocPosition::Target ? targetWithTags(pos) : sourceWithTags(pos); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog void targetInsertTag(const DocPosition&, const InlineTag&) override; InlineTag targetDeleteTag(const DocPosition&) override; QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override; QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override; QVector notes(const DocPosition& pos) const override; Note setNote(DocPosition pos, const Note& note) override; QVector altTrans(const DocPosition& pos) const override; QStringList sourceFiles(const DocPosition& pos) const override; QVector developerNotes(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isApproved(const DocPosition& pos) const override; void setApproved(const DocPosition& pos, bool approved) override; bool isEmpty(const DocPosition& pos) const override; QString mimetype() const override { return QStringLiteral("text/x-gettext-translation"); } QString fileType() const override { return QStringLiteral("Gettext (*.po)"); } CatalogType type() const override { return Gettext; } private: bool setHeader(const CatalogItem& newHeader); void setCodec(QTextCodec* codec) { m_codec = codec; } QVector notes(const DocPosition& pos, const QRegExp& re, int preLen) const; private: QVector m_entries; QVector m_obsoleteEntries; CatalogItem m_header; QTextCodec* m_codec; short m_maxLineLength; short m_trailingNewLines; bool m_generatedFromDocbook; QStringList m_catalogExtraData; QByteArray m_catalogExtraDataCompressed; friend class CatalogImportPlugin; friend class GettextExportPlugin; }; } #endif diff --git a/src/catalog/gettext/importplugin.cpp b/src/catalog/gettext/importplugin.cpp index b07af07..7c265a0 100644 --- a/src/catalog/gettext/importplugin.cpp +++ b/src/catalog/gettext/importplugin.cpp @@ -1,151 +1,152 @@ /* **************************************************************************** This file is part of Lokalize This file is based on the one from KBabel Copyright (C) 2002-2003 by Stanislav Visnovsky 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "lokalize_debug.h" #include "catalogfileplugin.h" #include "importplugin_private.h" #include "gettextstorage.h" #include #include #include namespace GettextCatalog { CatalogImportPlugin::CatalogImportPlugin() : _maxLineLength(0) , _trailingNewLines(0) , _errorLine(0) , d(new CatalogImportPluginPrivate) { } CatalogImportPlugin::~CatalogImportPlugin() { delete d; } void CatalogImportPlugin::appendCatalogItem(const CatalogItem& item, const bool obsolete) { if (item.msgid().isEmpty()) return; if (obsolete) d->_obsoleteEntries.append(item); else d->_entries.append(item); } void CatalogImportPlugin::setCatalogExtraData(const QStringList& data) { d->_catalogExtraData = data; d->_updateCatalogExtraData = true; } void CatalogImportPlugin::setGeneratedFromDocbook(const bool generated) { d->_generatedFromDocbook = generated; d->_updateGeneratedFromDocbook = true; } void CatalogImportPlugin::setErrorIndex(const QList& errors) { d->_errorList = errors; d->_updateErrorList = true; } void CatalogImportPlugin::setHeader(const CatalogItem& item) { d->_header = item; d->_updateHeader = true; } void CatalogImportPlugin::setCodec(QTextCodec* codec) { d->_codec = codec; } ConversionStatus CatalogImportPlugin::open(QIODevice* device, GettextStorage* catalog, int* line) { d->_catalog = catalog; startTransaction(); ConversionStatus result = load(device); if (result == OK || result == RECOVERED_PARSE_ERROR || result == RECOVERED_HEADER_ERROR) commitTransaction(); if (line) (*line) = _errorLine; return result; } void CatalogImportPlugin::startTransaction() { d->_updateCodec = false; d->_updateCatalogExtraData = false; d->_updateGeneratedFromDocbook = false; d->_updateErrorList = false; d->_updateHeader = false; d->_entries.clear(); } void CatalogImportPlugin::commitTransaction() { GettextStorage* catalog = d->_catalog; //catalog->clear(); // fill in the entries QVector& entries = catalog->m_entries; entries.reserve(d->_entries.count()); //d->_catalog->setEntries( e ); for (QLinkedList::const_iterator it = d->_entries.begin(); it != d->_entries.end(); ++it/*,++i*/) entries.append(*it); // The codec is specified in the header, so it must be updated before the header is. catalog->setCodec(d->_codec); catalog->m_catalogExtraData = d->_catalogExtraData; catalog->m_generatedFromDocbook = d->_generatedFromDocbook; catalog->setHeader(d->_header); //if( d->_updateErrorList ) d->_catalog->setErrorIndex(d->_errorList); catalog->m_maxLineLength = _maxLineLength; catalog->m_trailingNewLines = _trailingNewLines; } } diff --git a/src/catalog/gettextheader.cpp b/src/catalog/gettextheader.cpp index e239786..17e1dcf 100644 --- a/src/catalog/gettextheader.cpp +++ b/src/catalog/gettextheader.cpp @@ -1,672 +1,673 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 "gettextheader.h" #include "lokalize_debug.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include "prefs.h" #include #include #include #include #include #include #include #include #include #include /** * this data was obtained by running GNUPluralForms() * on all languages KDE knows of **/ struct langPInfo { const char *lang; const char *plural; }; static const langPInfo langsWithPInfo[] = { { "ar", "nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;" }, { "be@latin", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "be", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "br", "nplurals=1; plural=0;" }, { "bs", "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" }, { "csb", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)" }, { "cs", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" }, { "da", "nplurals=2; plural=(n != 1);" }, { "de", "nplurals=2; plural=(n != 1);" }, { "el", "nplurals=2; plural=(n != 1);" }, { "en", "nplurals=2; plural=(n != 1);" }, { "en_GB", "nplurals=2; plural=(n != 1);" }, { "en_US", "nplurals=2; plural=(n != 1);" }, { "eo", "nplurals=2; plural=(n != 1);" }, { "es", "nplurals=2; plural=(n != 1);" }, { "et", "nplurals=2; plural=(n != 1);" }, { "fa", "nplurals=1; plural=0;" }, { "fi", "nplurals=2; plural=(n != 1);" }, { "fo", "nplurals=2; plural=(n != 1);" }, { "fr", "nplurals=2; plural=(n > 1);" }, { "ga", "nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? 3 : 4" }, { "gd", "nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;" }, { "gu", "nplurals=2; plural=(n!=1);" }, { "he", "nplurals=2; plural=(n != 1);" }, { "hi", "nplurals=2; plural=(n!=1);" }, { "hne", "nplurals=2; plural=(n!=1);" }, { "hr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "hsb", "nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;" }, { "hu", "nplurals=2; plural=(n != 1);" }, { "hy", "nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" }, { "id", "nplurals=2; plural=(n != 1);" }, { "it", "nplurals=2; plural=(n != 1);" }, { "ja", "nplurals=1; plural=0;" }, { "ka", "nplurals=1; plural=0;" }, { "kk", "nplurals=1; plural=0;" }, { "km", "nplurals=1; plural=0;" }, { "ko", "nplurals=1; plural=0;" }, { "lt", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "lv", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" }, { "mai", "nplurals=2; plural=(n!=1);" }, { "mk", "nplurals=3; plural=n%10==1 ? 0 : n%10==2 ? 1 : 2;" }, { "mr", "nplurals=2; plural=(n!=1);" }, { "ms", "nplurals=2; plural=1;" }, { "nb", "nplurals=2; plural=(n != 1);" }, { "nl", "nplurals=2; plural=(n != 1);" }, { "nn", "nplurals=2; plural=(n != 1);" }, { "oc", "nplurals=2; plural=(n > 1);" }, { "or", "nplurals=2; plural=(n!=1);" }, { "pl", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "pt", "nplurals=2; plural=(n != 1);" }, { "pt_BR", "nplurals=2; plural=(n > 1);" }, { "ro", "nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;" }, { "ru", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sk", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" }, { "sl", "nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);" }, { "sr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sr@latin", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sv", "nplurals=2; plural=(n != 1);" }, { "te", "nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4;" }, { "th", "nplurals=1; plural=0;" }, { "tr", "nplurals=2; plural=(n > 1);" }, { "ug", "nplurals=1; plural=0;" }, { "uk", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "uz", "nplurals=1; plural=0;" }, { "uz@cyrillic", "nplurals=1; plural=0;" }, { "vi", "nplurals=1; plural=0;" }, { "zh_CN", "nplurals=1; plural=0;" }, { "zh_HK", "nplurals=1; plural=0;" }, { "zh_TW", "nplurals=1; plural=0;" } }; static const size_t langsWithPInfoCount = sizeof(langsWithPInfo) / sizeof(langsWithPInfo[0]); int numberOfPluralFormsFromHeader(const QString& header) { QRegExp rxplural(QStringLiteral("Plural-Forms:\\s*nplurals=(.);")); if (rxplural.indexIn(header) == -1) return 0; bool ok; int result = rxplural.cap(1).toShort(&ok); return ok ? result : 0; } int numberOfPluralFormsForLangCode(const QString& langCode) { QString expr = GNUPluralForms(langCode); QRegExp rxplural(QStringLiteral("nplurals=(.);")); if (rxplural.indexIn(expr) == -1) return 0; bool ok; int result = rxplural.cap(1).toShort(&ok); return ok ? result : 0; } QString GNUPluralForms(const QString& lang) { QByteArray l(lang.toUtf8()); int i = langsWithPInfoCount; while (--i >= 0 && l != langsWithPInfo[i].lang) ; if (Q_LIKELY(i >= 0)) return QString::fromLatin1(langsWithPInfo[i].plural); i = langsWithPInfoCount; while (--i >= 0 && !l.startsWith(langsWithPInfo[i].lang)) ; if (Q_LIKELY(i >= 0)) return QString::fromLatin1(langsWithPInfo[i].plural); //BEGIN alternative // NOTE does this work under M$ OS? qCDebug(LOKALIZE_LOG) << "gonna call msginit"; QString def = QStringLiteral("nplurals=2; plural=n != 1;"); QStringList arguments; arguments << QLatin1String("-l") << lang << QLatin1String("-i") << QLatin1String("-") << QLatin1String("-o") << QLatin1String("-") << QLatin1String("--no-translator") << QLatin1String("--no-wrap"); QProcess msginit; msginit.start(QLatin1String("msginit"), arguments); msginit.waitForStarted(5000); if (Q_UNLIKELY(msginit.state() != QProcess::Running)) { //qCWarning(LOKALIZE_LOG)<<"msginit error"; return def; } msginit.write( "# SOME DESCRIPTIVE TITLE.\n" "# Copyright (C) YEAR Free Software Foundation, Inc.\n" "# FIRST AUTHOR , YEAR.\n" "#\n" "#, fuzzy\n" "msgid \"\"\n" "msgstr \"\"\n" "\"Project-Id-Version: PACKAGE VERSION\\n\"\n" "\"POT-Creation-Date: 2002-06-25 03:23+0200\\n\"\n" "\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n" "\"Last-Translator: FULL NAME \\n\"\n" "\"Language-Team: LANGUAGE \\n\"\n" "\"Language: LL\\n\"\n" "\"MIME-Version: 1.0\\n\"\n" "\"Content-Type: text/plain; charset=UTF-8\\n\"\n" "\"Content-Transfer-Encoding: ENCODING\\n\"\n" // "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n" ); msginit.closeWriteChannel(); if (Q_UNLIKELY(!msginit.waitForFinished(5000))) { qCWarning(LOKALIZE_LOG) << "msginit error"; return def; } QByteArray result = msginit.readAll(); int pos = result.indexOf("Plural-Forms: "); if (Q_UNLIKELY(pos == -1)) { //qCWarning(LOKALIZE_LOG)<<"msginit error"<'); temp = QStringLiteral("Last-Translator: ") % authorNameEmail % BACKSLASH_N; QRegExp lt(QStringLiteral("^ *Last-Translator:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { if (it->contains(lt)) { if (forSaving) *it = temp; found = true; } } if (Q_UNLIKELY(!found)) headerList.append(temp); QLocale cLocale(QLocale::C); QString dateTimeString = cLocale.toString(QDateTime::currentDateTime(), QStringLiteral("yyyy-MM-dd hh:mm")); const int offset_seconds = QDateTime::currentDateTime().offsetFromUtc(); const int offset_hours = abs(offset_seconds) / 3600; const int offset_minutes = abs(offset_seconds % 3600) / 60; QString zoneOffsetString = (offset_seconds >= 0 ? '+' : '-') % (offset_hours < 10 ? QStringLiteral("0") : QStringLiteral("")) % QString::number(offset_hours) % (offset_minutes < 10 ? QStringLiteral("0") : QStringLiteral("")) % QString::number(offset_minutes); temp = QStringLiteral("PO-Revision-Date: ") % dateTimeString % zoneOffsetString % BACKSLASH_N; QRegExp poRevDate(QStringLiteral("^ *PO-Revision-Date:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(poRevDate); if (found && forSaving) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); temp = QStringLiteral("Project-Id-Version: ") % CatalogProjectId % BACKSLASH_N; //temp.replace( "@PACKAGE@", packageName()); QRegExp projectIdVer(QStringLiteral("^ *Project-Id-Version:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(projectIdVer); if (found && it->contains(QLatin1String("PACKAGE VERSION"))) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); langCode = Project::instance()->isLoaded() ? Project::instance()->langCode() : Settings::defaultLangCode(); QString language; //initialized with preexisting value or later QString mailingList; //initialized with preexisting value or later static QMap langEnums; if (!langEnums.size()) for (int l = QLocale::Abkhazian; l <= QLocale::Akoose; ++l) langEnums[QLocale::languageToString((QLocale::Language)l)] = (QLocale::Language)l; static QRegExp langTeamRegExp(QStringLiteral("^ *Language-Team:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(langTeamRegExp); if (found) { //really parse header QRegExp re(QStringLiteral("^ *Language-Team: *(.*) *<([^>]*)>")); if (re.indexIn(*it) != -1) { if (langEnums.contains(re.cap(1).trimmed())) { language = re.cap(1).trimmed(); mailingList = re.cap(2).trimmed(); QList locales = QLocale::matchingLocales(langEnums.value(language), QLocale::AnyScript, QLocale::AnyCountry); if (locales.size()) langCode = locales.first().name().left(2); } } ait = it; } } if (language.isEmpty()) { language = QLocale::languageToString(QLocale(langCode).language()); if (language.isEmpty()) language = langCode; } if (mailingList.isEmpty() || belongsToProject) { if (Project::instance()->isLoaded()) mailingList = Project::instance()->mailingList(); else //if (mailingList.isEmpty()) mailingList = Settings::defaultMailingList(); } temp = QStringLiteral("Language-Team: ") % language % QStringLiteral(" <") % mailingList % QStringLiteral(">\\n"); if (Q_LIKELY(found)) (*ait) = temp; else headerList.append(temp); static QRegExp langCodeRegExp(QStringLiteral("^ *Language: *([^ \\\\]*)")); temp = QStringLiteral("Language: ") % langCode % BACKSLASH_N; for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = (langCodeRegExp.indexIn(*it) != -1); if (found && langCodeRegExp.cap(1).isEmpty()) *it = temp; //if (found) qCWarning(LOKALIZE_LOG)<<"got explicit lang code:"<contains(ctRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); temp = QStringLiteral("Content-Transfer-Encoding: 8bit\\n"); QRegExp cteRe(QStringLiteral("^ *Content-Transfer-Encoding:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) found = it->contains(cteRe); if (!found) headerList.append(temp); // ensure MIME-Version header temp = QStringLiteral("MIME-Version: 1.0\\n"); QRegExp mvRe(QStringLiteral("^ *MIME-Version:")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(mvRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); //qCDebug(LOKALIZE_LOG)<<"testing for GNUPluralForms"; // update plural form header QRegExp pfRe(QStringLiteral("^ *Plural-Forms:")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) found = it->contains(pfRe); if (found) { --it; //qCDebug(LOKALIZE_LOG)<<"GNUPluralForms found"; int num = numberOfPluralFormsFromHeader(header); if (!num) { if (generatedFromDocbook) num = 1; else { qCDebug(LOKALIZE_LOG) << "No plural form info in header, using project-defined one" << langCode; QString t = GNUPluralForms(langCode); //qCWarning(LOKALIZE_LOG)<<"generated: " << t; if (!t.isEmpty()) { static QRegExp pf(QStringLiteral("^ *Plural-Forms:\\s*nplurals.*\\\\n")); pf.setMinimal(true); temp = QStringLiteral("Plural-Forms: %1\\n").arg(t); it->replace(pf, temp); num = numberOfPluralFormsFromHeader(temp); } else { qCWarning(LOKALIZE_LOG) << "no... smth went wrong :(\ncheck your gettext install"; num = 2; } } } numberOfPluralForms = num; } else if (!generatedFromDocbook) { //qCDebug(LOKALIZE_LOG)<<"generating GNUPluralForms"<contains(xgRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); //m_header.setMsgstr( headerList.join( "\n" ) ); header = headerList.join(QStringLiteral("\n")); //END header itself //BEGIN comment = description, copyrights // U+00A9 is the Copyright sign QRegExp fsfc(QStringLiteral("^# *Copyright (\\(C\\)|\\x00a9).*Free Software Foundation, Inc")); for (it = commentList.begin(), found = false; it != commentList.end() && !found; ++it) { found = it->contains(fsfc) ; if (found) it->replace(QStringLiteral("YEAR"), cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy"))); } /* if( saveOptions.FSFCopyright == ProjectSettingsBase::Update ) { //update years QString cy = cLocale.toString(QDate::currentDate(), "yyyy"); if( !it->contains( QRegExp(cy)) ) // is the year already included? { int index = it->lastIndexOf( QRegExp("[\\d]+[\\d\\-, ]*") ); if( index == -1 ) { KMessageBox::information(nullptr,i18n("Free Software Foundation Copyright does not contain any year. " "It will not be updated.")); } else { it->insert(index+1, QString(", ")+cy); } } }*/ #if 0 if ((!usePrefs || saveOptions.updateDescription) && (!saveOptions.descriptionString.isEmpty())) { temp = "# " + saveOptions.descriptionString; temp.replace("@PACKAGE@", packageName()); temp.replace("@LANGUAGE@", identityOptions.languageName); temp = temp.trimmed(); // The description strings has often buggy variants already in the file, these must be removed QString regexpstr = "^#\\s+" + QRegExp::escape(saveOptions.descriptionString.trimmed()) + "\\s*$"; regexpstr.replace("@PACKAGE@", ".*"); regexpstr.replace("@LANGUAGE@", ".*"); //qCDebug(LOKALIZE_LOG) << "REGEXPSTR: " << regexpstr; QRegExp regexp(regexpstr); // The buggy variants exist in English too (of a time before KBabel got a translation for the corresponding language) QRegExp regexpUntranslated("^#\\s+translation of .* to .*\\s*$"); qCDebug(LOKALIZE_LOG) << "Temp is '" << temp << "'"; found = false; bool foundTemplate = false; it = commentList.begin(); while (it != commentList.end()) { qCDebug(LOKALIZE_LOG) << "testing '" << (*it) << "'"; bool deleteItem = false; if ((*it) == temp) { qCDebug(LOKALIZE_LOG) << "Match "; if (found) deleteItem = true; else found = true; } else if (regexp.indexIn(*it) >= 0) { // We have a similar (translated) string (from another project or another language (perhaps typos)). Remove it. deleteItem = true; } else if (regexpUntranslated.indexIn(*it) >= 0) { // We have a similar (untranslated) string (from another project or another language (perhaps typos)). Remove it. deleteItem = true; } else if ((*it) == "# SOME DESCRIPTIVE TITLE.") { // We have the standard title placeholder, remove it deleteItem = true; } if (deleteItem) it = commentList.erase(it); else ++it; } if (!found) commentList.prepend(temp); } #endif // qCDebug(LOKALIZE_LOG) << "HEADER COMMENT: " << commentList; /* if ( (!usePrefs || saveOptions.updateTranslatorCopyright) && ( ! identityOptions->readEntry("authorName","").isEmpty() ) && ( ! identityOptions->readEntry("Email","").isEmpty() ) ) // An email address can be used as ersatz of a name {*/ // return; QStringList foundAuthors; temp = QStringLiteral("# ") % authorNameEmail % QStringLiteral(", ") % cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy")) % '.'; // ### TODO: it would be nice if the entry could start with "COPYRIGHT" and have the "(C)" symbol (both not mandatory) QRegExp regexpAuthorYear(QStringLiteral("^#.*(<.+@.+>)?,\\s*([\\d]+[\\d\\-, ]*|YEAR)")); QRegExp regexpYearAlone(QStringLiteral("^# , \\d{4}.?\\s*$")); if (commentList.isEmpty()) { commentList.append(temp); commentList.append(QString()); } else { it = commentList.begin(); while (it != commentList.end()) { bool deleteItem = false; if (it->indexOf(QLatin1String("copyright"), 0, Qt::CaseInsensitive) != -1) { // We have a line with a copyright. It should not be moved. } else if (it->contains(QRegExp(QStringLiteral("#, *fuzzy")))) deleteItem = true; else if (it->contains(regexpYearAlone)) { // We have found a year number that is preceded by a comma. // That is typical of KBabel 1.10 (and earlier?) when there is neither an author name nor an email // Remove the entry deleteItem = true; } else if (it->contains(QLatin1String("# FIRST AUTHOR , YEAR."))) deleteItem = true; else if (it->contains(QLatin1String("# SOME DESCRIPTIVE TITLE"))) deleteItem = true; else if (it->contains(regexpAuthorYear)) { // email address followed by year if (!foundAuthors.contains((*it))) { // The author line is new (and not a duplicate), so add it to the author line list foundAuthors.append((*it)); } // Delete also non-duplicated entry, as now all what is needed will be processed in foundAuthors deleteItem = true; } if (deleteItem) it = commentList.erase(it); else ++it; } if (!foundAuthors.isEmpty()) { found = false; bool foundAuthor = false; const QString cy = cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy")); ait = foundAuthors.end(); for (it = foundAuthors.begin() ; it != foundAuthors.end(); ++it) { if (it->contains(Settings::authorName()) || it->contains(Settings::authorEmail())) { foundAuthor = true; if (it->contains(cy)) found = true; else ait = it; } } if (!found) { if (!foundAuthor) foundAuthors.append(temp); else if (ait != foundAuthors.end()) { //update years const int index = (*ait).lastIndexOf(QRegExp(QStringLiteral("[\\d]+[\\d\\-, ]*"))); if (index == -1) (*ait) += QStringLiteral(", ") % cy; else ait->insert(index + 1, QStringLiteral(", ") % cy); } else qCDebug(LOKALIZE_LOG) << "INTERNAL ERROR: author found but iterator dangling!"; } } else foundAuthors.append(temp); foreach (QString author, foundAuthors) { // ensure dot at the end of copyright if (!author.endsWith(QLatin1Char('.'))) author += QLatin1Char('.'); commentList.append(author); } } //m_header.setComment( commentList.join( "\n" ) ); comment = commentList.join(QStringLiteral("\n")); //END comment = description, copyrights } QString fullUserName();// defined in helpers.cpp bool askAuthorInfoIfEmpty() { if (QThread::currentThread() == qApp->thread()) { if (Settings::authorName().isEmpty()) { bool ok; QString contact = QInputDialog::getText( SettingsController::instance()->mainWindowPtr(), i18nc("@window:title", "Author name missing"), i18n("Your name:"), QLineEdit::Normal, fullUserName(), &ok); Settings::self()->authorNameItem()->setValue(ok ? contact : fullUserName()); Settings::self()->save(); } if (Settings::authorEmail().isEmpty()) { bool ok; QString email = QInputDialog::getText( SettingsController::instance()->mainWindowPtr(), i18nc("@window:title", "Author email missing"), i18n("Your email:"), QLineEdit::Normal, QString(), &ok); if (ok) { Settings::self()->authorEmailItem()->setValue(email); Settings::self()->save(); } } } return !Settings::authorName().isEmpty() && !Settings::authorEmail().isEmpty(); } diff --git a/src/catalog/gettextheader.h b/src/catalog/gettextheader.h index 905c916..91e6493 100644 --- a/src/catalog/gettextheader.h +++ b/src/catalog/gettextheader.h @@ -1,51 +1,52 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 GETTEXTHEADER_H #define GETTEXTHEADER_H #include #include int numberOfPluralFormsFromHeader(const QString& header); QString GNUPluralForms(const QString& lang); void updateHeader(QString& header, QString& comment, QString& langCode, int& numberOfPluralForms, const QString& CatalogProjectId, bool generatedFromDocbook, bool belongsToProject, bool forSaving, QTextCodec* codec); //for XLIFF int numberOfPluralFormsForLangCode(const QString& langCode); /// @returns false if author info is still empty after function finishes bool askAuthorInfoIfEmpty(); #endif diff --git a/src/catalog/note.h b/src/catalog/note.h index b123e38..a01a7de 100644 --- a/src/catalog/note.h +++ b/src/catalog/note.h @@ -1,60 +1,61 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 NOTE_H #define NOTE_H #include struct Note { enum Owner {General, Source, Target}; QString content; char priority;//1 is the highest Owner annotates; QString from; QString lang; Note(const QString& content_ = QString()) : content(content_) , priority(5) , annotates(General) {} Note(const QString& content_, char priority_, Owner annotates_, const QString& from_, const QString& lang_) : content(content_) , priority(priority_) , annotates(annotates_) , from(from_) , lang(lang_) {} bool operator<(const Note& other) const { return priority < other.priority; } }; #endif diff --git a/src/catalog/phase.cpp b/src/catalog/phase.cpp index 49cb6e7..216e0a6 100644 --- a/src/catalog/phase.cpp +++ b/src/catalog/phase.cpp @@ -1,95 +1,96 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "phase.h" #include "cmd.h" #include "catalog.h" #include "project.h" #include "prefs_lokalize.h" #include "gettextheader.h" #include #include const char* const* processes() { static const char* const processes[] = {"translation", "review", "approval"}; return processes; } //guess role ProjectLocal::PersonRole roleForProcess(const QString& process) { int i = ProjectLocal::Undefined; while (i >= 0 && !process.startsWith(processes()[--i])) ; return (i == -1) ? Project::local()->role() : ProjectLocal::PersonRole(i); } void generatePhaseForCatalogIfNeeded(Catalog* catalog) { if (Q_LIKELY(!(catalog->capabilities()&Phases) || catalog->activePhaseRole() == ProjectLocal::Undefined)) return; Phase phase; phase.process = processes()[Project::local()->role()]; if (initPhaseForCatalog(catalog, phase)) static_cast(catalog)->push(new UpdatePhaseCmd(catalog, phase)); catalog->setActivePhase(phase.name, roleForProcess(phase.process)); } bool initPhaseForCatalog(Catalog* catalog, Phase& phase, int options) { askAuthorInfoIfEmpty(); phase.contact = Settings::authorName(); QSet names; QList phases = catalog->allPhases(); qSort(phases.begin(), phases.end(), qGreater()); foreach (const Phase& p, phases) { if (!(options & ForceAdd) && p.contact == phase.contact && p.process == phase.process) { phase = p; break; } names.insert(p.name); } if (phase.name.isEmpty()) { int i = 0; while (names.contains(phase.name = phase.process + QStringLiteral("-%1").arg(++i))) ; phase.date = QDate::currentDate(); phase.email = Settings::authorEmail(); return true; } return false; } Phase::Phase() : date(QDate::currentDate()) , tool(QStringLiteral("lokalize-" LOKALIZE_VERSION)) {} diff --git a/src/catalog/phase.h b/src/catalog/phase.h index b5196c9..4a40ce0 100644 --- a/src/catalog/phase.h +++ b/src/catalog/phase.h @@ -1,78 +1,79 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 PHASE_H #define PHASE_H #include "version.h" #include "projectlocal.h" #include #include class Catalog; struct Phase { QString name; QString process; QString company; QDate date; QString contact; QString email; QString phone; QString tool; Phase(); Phase(const Phase& rhs) : name(rhs.name) , process(rhs.process) , company(rhs.company) , date(rhs.date) , contact(rhs.contact) , email(rhs.email) , phone(rhs.phone) , tool(rhs.tool) {} bool operator<(const Phase& other) const { return date < other.date; } }; struct Tool { QString tool; QString name; QString version; QString company; }; const char* const* processes(); ProjectLocal::PersonRole roleForProcess(const QString& phase); enum InitOptions {ForceAdd = 1}; ///@returns true if phase must be added to catalog; bool initPhaseForCatalog(Catalog* catalog, Phase& phase, int options = 0); void generatePhaseForCatalogIfNeeded(Catalog* catalog); #endif diff --git a/src/catalog/pos.cpp b/src/catalog/pos.cpp index 5a9cfb9..f391868 100644 --- a/src/catalog/pos.cpp +++ b/src/catalog/pos.cpp @@ -1,152 +1,153 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "pos.h" #include "catalog.h" bool switchPrev(Catalog*& catalog, DocPosition& pos, int parts) { bool switchEntry = false; bool switchCommentIndex = false; if (pos.part == DocPosition::Comment) switchCommentIndex = true; else if (pos.part == DocPosition::Target) { if (parts & DocPosition::Source) pos.part = DocPosition::Source; switchEntry = !(parts & DocPosition::Source); } else if (pos.part == DocPosition::Source) switchEntry = true; bool skipCommentThisTime = false; if (switchCommentIndex) { if (pos.form) pos.form--; switchEntry = pos.form; //pos.form is zero again skipCommentThisTime = pos.form; } if (!switchEntry) return true; if (Q_UNLIKELY(pos.form > 0 && catalog->isPlural(pos.entry))) pos.form--; else if (Q_UNLIKELY(pos.entry == 0)) return false; else { pos.entry--; pos.form = catalog->isPlural(pos.entry) * (catalog->numberOfPluralForms() - 1); } pos.offset = 0; if (parts & DocPosition::Comment && !skipCommentThisTime && pos.form == 0 && catalog->notes(pos).size()) { pos.part = DocPosition::Comment; pos.form = catalog->notes(pos).size() - 1; } else pos.part = DocPosition::Target; return true; } bool switchNext(Catalog*& catalog, DocPosition& pos, int parts) { bool switchEntry = false; bool switchCommentIndex = false; if (pos.part == DocPosition::Source) pos.part = DocPosition::Target; else if (pos.part == DocPosition::Target) { if (parts & DocPosition::Comment && pos.form == 0 && catalog->notes(pos).size()) pos.part = DocPosition::Comment; else switchEntry = true; } else if (pos.part == DocPosition::Comment) switchCommentIndex = true; if (switchCommentIndex) { pos.form++; if (catalog->notes(pos).size() == pos.form) { pos.form = 0; switchEntry = true; } } if (!switchEntry) return true; if (Q_UNLIKELY(pos.entry != -1 && pos.form + 1 < catalog->numberOfPluralForms() && catalog->isPlural(pos.entry))) pos.form++; else if (Q_UNLIKELY(pos.entry == catalog->numberOfEntries() - 1)) return false; else { pos.entry++; pos.form = 0; } pos.offset = 0; pos.part = (parts & DocPosition::Source) ? DocPosition::Source : DocPosition::Target; return true; } #include const QDBusArgument &operator>>(const QDBusArgument &argument, DocPosition& pos) { int entry; int form; uint offset; argument.beginStructure(); argument >> entry >> form >> offset; argument.endStructure(); pos.entry = entry; pos.form = form; pos.offset = offset; return argument; } QDBusArgument &operator<<(QDBusArgument &argument, const DocPosition &pos) { int entry = pos.entry; int form = pos.form; uint offset = pos.offset; argument.beginStructure(); argument << entry << form << offset; argument.endStructure(); return argument; } diff --git a/src/catalog/state.h b/src/catalog/state.h index c049811..21a055c 100644 --- a/src/catalog/state.h +++ b/src/catalog/state.h @@ -1,43 +1,44 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 STATE_H #define STATE_H ///@see http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#state enum TargetState { New, NeedsTranslation, NeedsL10n, NeedsAdaptation, Translated, NeedsReviewTranslation, NeedsReviewL10n, NeedsReviewAdaptation, Final, SignedOff, StateCount }; #endif diff --git a/src/catalog/ts/tsstorage.cpp b/src/catalog/ts/tsstorage.cpp index 03a50dd..5e1b570 100644 --- a/src/catalog/ts/tsstorage.cpp +++ b/src/catalog/ts/tsstorage.cpp @@ -1,559 +1,560 @@ /* Copyright 2008-2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "tsstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif //static const char* const noyes[]={"no","yes"}; static const QString names[] = {U("source"), U("translation"), U("oldsource"), U("translatorcomment"), U("comment"), U("name"), U("numerus")}; enum TagNames {SourceTag, TargetTag, OldSourceTag, NoteTag, DevNoteTag, NameTag, PluralTag}; static const QString attrnames[] = {U("location"), U("type"), U("obsolete")}; enum AttrNames {LocationAttr, TypeAttr, ObsoleteAttr}; static const QString attrvalues[] = {U("obsolete"), U("vanished")}; enum AttValues {ObsoleteVal, VanishedVal}; TsStorage::TsStorage() : CatalogStorage() { } TsStorage::~TsStorage() { } int TsStorage::capabilities() const { return 0;//MultipleNotes; } //BEGIN OPEN/SAVE int TsStorage::load(QIODevice* device) { QTime chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); if (!success) { qCWarning(LOKALIZE_LOG) << "parse error" << errorMsg << errorLine; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("sourcelanguage")); m_targetLangCode = file.attribute(QStringLiteral("language")); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("message")); qCWarning(LOKALIZE_LOG) << chrono.elapsed() << "secs, " << entries.size() << "entries"; return 0; } bool TsStorage::save(QIODevice* device, bool belongsToProject) { Q_UNUSED(belongsToProject) QTextStream stream(device); m_doc.save(stream, 4); return true; } //END OPEN/SAVE //BEGIN STORAGE TRANSLATION int TsStorage::size() const { //return m_map.size(); return entries.size(); } /** * helper structure used during XLIFF XML walk-through */ struct TsContentEditingData { enum ActionType {Get, DeleteText, InsertText, CheckLength}; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get TsContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText TsContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText TsContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} }; static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on TsContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, TsContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, TsContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == TsContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == TsContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == TsContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = TsContentEditingData::CheckLength; return QString('a');//so it exits 100% } cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString TsStorage::catalogString(QDomElement contentElement) const { CatalogString catalogString; TsContentEditingData data(TsContentEditingData::Get); catalogString.string = content(contentElement, &data); return catalogString; } CatalogString TsStorage::catalogString(const DocPosition& pos) const { return catalogString(pos.part == DocPosition::Target ? targetForPos(pos) : sourceForPos(pos.entry)); } CatalogString TsStorage::targetWithTags(DocPosition pos) const { return catalogString(targetForPos(pos)); } CatalogString TsStorage::sourceWithTags(DocPosition pos) const { return catalogString(sourceForPos(pos.entry)); } QString TsStorage::source(const DocPosition& pos) const { return content(sourceForPos(pos.entry)); } QString TsStorage::target(const DocPosition& pos) const { return content(targetForPos(pos)); } QString TsStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = source(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } QString TsStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = target(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } void TsStorage::targetDelete(const DocPosition& pos, int count) { TsContentEditingData data(pos.offset, count); content(targetForPos(pos), &data); } void TsStorage::targetInsert(const DocPosition& pos, const QString& arg) { qCWarning(LOKALIZE_LOG) << pos.entry << arg; QDomElement targetEl = targetForPos(pos); //BEGIN add <*target> if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(names[SourceTag]); targetEl = unitEl.insertAfter(m_doc.createElement(names[TargetTag]), refNode).toElement(); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag TsContentEditingData data(pos.offset, arg); content(targetEl, &data); } void TsStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector TsStorage::altTrans(const DocPosition& pos) const { QVector result; QString oldsource = content(unitForPos(pos.entry).firstChildElement(names[OldSourceTag])); if (!oldsource.isEmpty()) result << AltTrans(CatalogString(oldsource), i18n("Previous source value, saved by lupdate tool")); return result; } QStringList TsStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(attrnames[LocationAttr]); while (!elem.isNull()) { QString sourcefile = elem.attribute(QStringLiteral("filename")); QString linenumber = elem.attribute(QStringLiteral("line")); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile + ':' + linenumber); elem = elem.nextSiblingElement(attrnames[LocationAttr]); } //qSort(result); return result; } QVector TsStorage::notes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[NoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[NoteTag]); } return result; } QVector TsStorage::developerNotes(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(names[DevNoteTag]); while (!elem.isNull()) { Note note; note.content = elem.text(); result.append(note); elem = elem.nextSiblingElement(names[DevNoteTag]); } return result; } Note TsStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< if needed QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); //asking directly to bypass plural state detection if (target.attribute(attrnames[TypeAttr]) == attrvalues[ObsoleteVal]) return; if (approved) target.removeAttribute(attrnames[TypeAttr]); else target.setAttribute(attrnames[TypeAttr], QStringLiteral("unfinished")); } bool TsStorage::isApproved(const DocPosition& pos) const { QDomElement target = unitForPos(pos.entry).firstChildElement(names[TargetTag]); return !target.hasAttribute(attrnames[TypeAttr]) || target.attribute(attrnames[TypeAttr]) == attrvalues[VanishedVal]; } bool TsStorage::isObsolete(int entry) const { QDomElement target = unitForPos(entry).firstChildElement(names[TargetTag]); QString v = target.attribute(attrnames[TypeAttr]); return v == attrvalues[ObsoleteVal] || v == attrvalues[VanishedVal]; } bool TsStorage::isEmpty(const DocPosition& pos) const { TsContentEditingData data(TsContentEditingData::CheckLength); return content(targetForPos(pos), &data).isEmpty(); } bool TsStorage::isEquivTrans(const DocPosition& pos) const { Q_UNUSED(pos) return true;//targetForPos(pos.entry).attribute("equiv-trans")!="no"; } void TsStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { Q_UNUSED(pos) Q_UNUSED(equivTrans) //targetForPos(pos.entry).setAttribute("equiv-trans",noyes[equivTrans]); } QDomElement TsStorage::unitForPos(int pos) const { return entries.at(pos).toElement(); } QDomElement TsStorage::targetForPos(DocPosition pos) const { QDomElement unit = unitForPos(pos.entry); QDomElement translation = unit.firstChildElement(names[TargetTag]); if (!unit.hasAttribute(names[PluralTag])) return translation; if (pos.form == -1) pos.form = 0; QDomNodeList forms = translation.elementsByTagName(QStringLiteral("numerusform")); while (pos.form >= forms.size()) translation.appendChild(unit.ownerDocument().createElement(QStringLiteral("numerusform"))); return forms.at(pos.form).toElement(); } QDomElement TsStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(names[SourceTag]); } void TsStorage::setTargetLangCode(const QString& langCode) { m_targetLangCode = langCode; QDomElement file = m_doc.elementsByTagName(QStringLiteral("TS")).at(0).toElement(); if (m_targetLangCode != file.attribute(QStringLiteral("language")).replace('-', '_')) { QString l = langCode; file.setAttribute(QStringLiteral("language"), l.replace('_', '-')); } } //END STORAGE TRANSLATION diff --git a/src/catalog/ts/tsstorage.h b/src/catalog/ts/tsstorage.h index 8ad8d1a..4628a6e 100644 --- a/src/catalog/ts/tsstorage.h +++ b/src/catalog/ts/tsstorage.h @@ -1,121 +1,122 @@ /* Copyright 2008-2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TSSTORAGE_H #define TSSTORAGE_H #include "catalogstorage.h" #include #include #include #include // class QDomDocument; class TsStorage: public CatalogStorage { public: TsStorage(); ~TsStorage() override; int capabilities() const override; int load(QIODevice* device) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; bool isEmpty() const; //flat-model interface (ignores TS grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override; /// all plural forms. pos.form doesn't matter TODO QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog QStringList sourceFiles(const DocPosition& pos) const override; QVector altTrans(const DocPosition& pos) const override; ///@a pos.form is note number Note setNote(DocPosition pos, const Note& note) override; QVector notes(const DocPosition& pos) const override; QVector developerNotes(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isEmpty(const DocPosition& pos) const override; bool isEquivTrans(const DocPosition& pos) const override; void setEquivTrans(const DocPosition& pos, bool equivTrans) override; bool isApproved(const DocPosition& pos) const override; void setApproved(const DocPosition& pos, bool approved) override; bool isObsolete(int entry) const override; QString mimetype() const override { return QStringLiteral("application/x-linguist"); } QString fileType() const override { return QStringLiteral("Qt Linguist (*.ts)"); } CatalogType type() const override { return Ts; } void setTargetLangCode(const QString& langCode) override; private: QDomElement unitForPos(int pos) const; QDomElement targetForPos(DocPosition pos) const; QDomElement sourceForPos(int pos) const; CatalogString catalogString(QDomElement contentElement) const; private: mutable QDomDocument m_doc; QDomNodeList entries; }; #endif diff --git a/src/catalog/xliff/xliffstorage.cpp b/src/catalog/xliff/xliffstorage.cpp index 5e437b9..6e53fc1 100644 --- a/src/catalog/xliff/xliffstorage.cpp +++ b/src/catalog/xliff/xliffstorage.cpp @@ -1,1039 +1,1040 @@ /* Copyright 2008-2009 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "xliffstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif static const QString noyes[] = {U("no"), U("yes")}; static const QString bintargettarget[] = {U("bin-target"), U("target")}; static const QString binsourcesource[] = {U("bin-source"), U("source")}; static const QString NOTE = U("note"); XliffStorage::XliffStorage() : CatalogStorage() { } XliffStorage::~XliffStorage() { } int XliffStorage::capabilities() const { return KeepsNoteAuthors | MultipleNotes | Phases | ExtendedStates | Tags; } //BEGIN OPEN/SAVE int XliffStorage::load(QIODevice* device) { QTime chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); QString FILE = QStringLiteral("file"); if (!success || m_doc.elementsByTagName(FILE).isEmpty()) { qCWarning(LOKALIZE_LOG) << errorMsg; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(FILE).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("source-language")).replace(u'-', u'_'); m_targetLangCode = file.attribute(QStringLiteral("target-language")).replace(u'-', u'_'); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("trans-unit")); int size = entries.size(); m_map.clear(); m_map.reserve(size); for (int i = 0; i < size; ++i) { QDomElement parentElement = entries.at(i).parentNode().toElement(); //if (Q_UNLIKELY( e.isNull() ))//sanity // continue; m_map << i; m_unitsById[entries.at(i).toElement().attribute(QStringLiteral("id"))] = i; if (parentElement.tagName() == QLatin1String("group") && parentElement.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) { m_plurals.insert(i); int localPluralNum = m_numberOfPluralForms; while (--localPluralNum > 0 && (++i) < size) { QDomElement p = entries.at(i).parentNode().toElement(); if (p.tagName() == QLatin1String("group") && p.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) continue; parentElement.appendChild(entries.at(m_map.last()).cloneNode()); } } } binEntries = m_doc.elementsByTagName(QStringLiteral("bin-unit")); size = binEntries.size(); int offset = m_map.size(); for (int i = 0; i < size; ++i) m_unitsById[binEntries.at(i).toElement().attribute(QStringLiteral("id"))] = offset + i; // entries=m_doc.elementsByTagName("body"); // uint i=0; // uint lim=size(); // while (i msgstr(item.msgstrPlural()); // while (msgstr.count() tags; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get ContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText ContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText ContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} ///InsertTag ContentEditingData(int p, const InlineTag& range) : pos(p) , lengthOfStringToRemove(-1) , actionType(InsertTag) { tags.append(range); } ///DeleteTag ContentEditingData(int p) : pos(p) , lengthOfStringToRemove(-1) , actionType(DeleteTag) {} }; static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on ContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, ContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == ContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == ContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == ContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } //BEGIN INSERT TAG else if (data->actionType == ContentEditingData::InsertTag) { const InlineTag& tag = data->tags.first(); QString mid = cData.mid(localStartPos); qCDebug(LOKALIZE_LOG) << "inserting tag" << tag.name() << tag.id << tag.start << tag.end << mid << data->pos << startingPos; if (mid.size()) c.deleteData(localStartPos, mid.size()); QDomElement newNode = elem.insertAfter(elem.ownerDocument().createElement(tag.getElementName()), n).toElement(); newNode.setAttribute(QStringLiteral("id"), tag.id); if (!tag.xid.isEmpty()) newNode.setAttribute(QStringLiteral("xid"), tag.xid); if (tag.isPaired() && tag.end > (tag.start + 1)) { //qCWarning(LOKALIZE_LOG)<<"isPaired"; int len = tag.end - tag.start - 1; //-image symbol int localLen = qMin(len, mid.size()); if (localLen) { //appending text //qCWarning(LOKALIZE_LOG)<<"localLen. appending"<missingLen (or siblings end) int childrenCumulativeLen = 0; QDomNode sibling = newNode.nextSibling(); while (!sibling.isNull()) { //&&(childrenCumulativeLen missingLen) { if (tmp.isCharacterData()) { //divide the last string const QString& endData = tmp.toCharacterData().data(); QString last = endData.left(endData.size() - (childrenCumulativeLen - missingLen)); newNode.appendChild(elem.ownerDocument().createTextNode(last)); tmp.toCharacterData().deleteData(0, last.size()); //qCWarning(LOKALIZE_LOG)<<"end of add"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END INSERT TAG cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } else if (n.isElement()) { QDomElement el = n.toElement(); //BEGIN DELETE TAG if (data && data->actionType == ContentEditingData::DeleteTag && data->pos == startingPos) { //qCWarning(LOKALIZE_LOG)<<"start deleting tag"; data->tags.append(InlineTag(startingPos, -1, InlineTag::getElementType(el.tagName().toUtf8()), el.attribute("id"), el.attribute("xid"))); if (data->tags.first().isPaired()) { //get end position ContentEditingData subData(ContentEditingData::Get); QString subContent = doContent(el, startingPos, &subData); data->tags[0].end = 1 + startingPos + subContent.size(); //tagsymbol+text //qCWarning(LOKALIZE_LOG)<<"get end position"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END DELETE TAG if (!seenCharacterDataAfterElement) //add empty charData child so that user could add some text elem.insertBefore(elem.ownerDocument().createTextNode(QString()), n); seenCharacterDataAfterElement = false; if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } int oldStartingPos = startingPos; //detect type of the tag InlineTag::InlineElement i = InlineTag::getElementType(el.tagName().toUtf8()); //1 or 2 images to represent it? //2 = there may be content inside if (InlineTag::isPaired(i)) { QString recursiveContent = doContent(el, startingPos, data); if (!recursiveContent.isEmpty()) { result += recursiveContent; startingPos += recursiveContent.size(); } if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } } if (data && data->actionType == ContentEditingData::Get) { QString id = el.attribute(QStringLiteral("id")); if (i == InlineTag::mrk) //TODO attr map id = el.attribute(QStringLiteral("mtype")); //qCWarning(LOKALIZE_LOG)<<"tagName"<tags.append(InlineTag(oldStartingPos - 1, startingPos - 1, i, id, el.attribute(QStringLiteral("xid")))); } } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString XliffStorage::catalogString(QDomElement unit, DocPosition::Part part) const { static const QString names[] = {U("source"), U("target"), U("seg-source")}; CatalogString catalogString; ContentEditingData data(ContentEditingData::Get); int nameIndex = part == DocPosition::Target; if (nameIndex == 0 && !unit.firstChildElement(names[2]).isNull()) nameIndex = 2; catalogString.string = content(unit.firstChildElement(names[nameIndex]), &data); catalogString.tags = data.tags; return catalogString; } CatalogString XliffStorage::catalogString(const DocPosition& pos) const { return catalogString(unitForPos(pos.entry), pos.part); } CatalogString XliffStorage::targetWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Target); } CatalogString XliffStorage::sourceWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Source); } static QString genericContent(QDomElement elem, bool nonbin) { return nonbin ? content(elem) : elem.firstChildElement(QStringLiteral("external-file")).attribute(QStringLiteral("href")); } QString XliffStorage::source(const DocPosition& pos) const { return genericContent(sourceForPos(pos.entry), pos.entry < size()); } QString XliffStorage::target(const DocPosition& pos) const { return genericContent(targetForPos(pos.entry), pos.entry < size()); } QString XliffStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = source(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } QString XliffStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = target(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } void XliffStorage::targetDelete(const DocPosition& pos, int count) { if (pos.entry < size()) { ContentEditingData data(pos.offset, count); content(targetForPos(pos.entry), &data); } else { //only bulk delete requests are generated targetForPos(pos.entry).firstChildElement(QStringLiteral("external-file")).setAttribute(QStringLiteral("href"), QString()); } } void XliffStorage::targetInsert(const DocPosition& pos, const QString& arg) { //qCWarning(LOKALIZE_LOG)<<"targetinsert"< if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(QStringLiteral("seg-source")); //obey standard if (refNode.isNull()) refNode = unitEl.firstChildElement(binsourcesource[pos.entry < size()]); targetEl = unitEl.insertAfter(m_doc.createElement(bintargettarget[pos.entry < size()]), refNode).toElement(); targetEl.setAttribute(QStringLiteral("state"), QStringLiteral("new")); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag if (pos.entry >= size()) { QDomElement ef = targetEl.firstChildElement(QStringLiteral("external-file")); if (ef.isNull()) ef = targetEl.appendChild(m_doc.createElement(QStringLiteral("external-file"))).toElement(); ef.setAttribute(QStringLiteral("href"), arg); return; } ContentEditingData data(pos.offset, arg); content(targetEl, &data); } void XliffStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { targetInsert(pos, QString()); //adds if needed ContentEditingData data(tag.start, tag); content(targetForPos(pos.entry), &data); } InlineTag XliffStorage::targetDeleteTag(const DocPosition& pos) { ContentEditingData data(pos.offset); content(targetForPos(pos.entry), &data); if (data.tags[0].end == -1) data.tags[0].end = data.tags[0].start; return data.tags.first(); } void XliffStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector XliffStorage::altTrans(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("alt-trans")); while (!elem.isNull()) { AltTrans aTrans; aTrans.source = catalogString(elem, DocPosition::Source); aTrans.target = catalogString(elem, DocPosition::Target); aTrans.phase = elem.attribute(QStringLiteral("phase-name")); aTrans.origin = elem.attribute(QStringLiteral("origin")); aTrans.score = elem.attribute(QStringLiteral("match-quality")).toInt(); aTrans.lang = elem.firstChildElement(QStringLiteral("target")).attribute(QStringLiteral("xml:lang")); const char* const types[] = { "proposal", "previous-version", "rejected", "reference", "accepted" }; QString typeStr = elem.attribute(QStringLiteral("alttranstype")); int i = -1; while (++i < int(sizeof(types) / sizeof(char*)) && types[i] != typeStr) ; aTrans.type = AltTrans::Type(i); result << aTrans; elem = elem.nextSiblingElement(QStringLiteral("alt-trans")); } return result; } static QDomElement phaseElement(QDomDocument m_doc, const QString& name, QDomElement& phasegroup) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); phasegroup = header.firstChildElement(QStringLiteral("phase-group")); if (phasegroup.isNull()) { phasegroup = m_doc.createElement(QStringLiteral("phase-group")); //order following XLIFF spec QDomElement skl = header.firstChildElement(QStringLiteral("skl")); if (!skl.isNull()) header.insertAfter(phasegroup, skl); else header.insertBefore(phasegroup, header.firstChildElement()); } QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull() && phaseElem.attribute(QStringLiteral("phase-name")) != name) phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); return phaseElem; } static Phase phaseFromElement(QDomElement phaseElem) { Phase phase; phase.name = phaseElem.attribute(QStringLiteral("phase-name")); phase.process = phaseElem.attribute(QStringLiteral("process-name")); phase.company = phaseElem.attribute(QStringLiteral("company-name")); phase.contact = phaseElem.attribute(QStringLiteral("contact-name")); phase.email = phaseElem.attribute(QStringLiteral("contact-email")); phase.phone = phaseElem.attribute(QStringLiteral("contact-phone")); phase.tool = phaseElem.attribute(QStringLiteral("tool-id")); phase.date = QDate::fromString(phaseElem.attribute(QStringLiteral("date")), Qt::ISODate); return phase; } Phase XliffStorage::updatePhase(const Phase& phase) { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phase.name, phasegroup); Phase prev = phaseFromElement(phaseElem); if (phaseElem.isNull() && !phase.name.isEmpty()) { phaseElem = phasegroup.appendChild(m_doc.createElement(QStringLiteral("phase"))).toElement(); phaseElem.setAttribute(QStringLiteral("phase-name"), phase.name); } phaseElem.setAttribute(QStringLiteral("process-name"), phase.process); if (!phase.company.isEmpty()) phaseElem.setAttribute(QStringLiteral("company-name"), phase.company); phaseElem.setAttribute(QStringLiteral("contact-name"), phase.contact); phaseElem.setAttribute(QStringLiteral("contact-email"), phase.email); //Q_ASSERT(phase.contact.length()); //is empty when exiting w/o saving if (!phase.phone.isEmpty()) phaseElem.setAttribute(QLatin1String("contact-phone"), phase.phone); phaseElem.setAttribute(QStringLiteral("tool-id"), phase.tool); if (phase.date.isValid()) phaseElem.setAttribute(QStringLiteral("date"), phase.date.toString(Qt::ISODate)); return prev; } QList XliffStorage::allPhases() const { QList result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement phasegroup = header.firstChildElement(QStringLiteral("phase-group")); QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull()) { result.append(phaseFromElement(phaseElem)); phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); } return result; } Phase XliffStorage::phase(const QString& name) const { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, name, phasegroup); return phaseFromElement(phaseElem); } QMap XliffStorage::allTools() const { QMap result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement toolElem = header.firstChildElement(QStringLiteral("tool")); while (!toolElem.isNull()) { Tool tool; tool.tool = toolElem.attribute(QStringLiteral("tool-id")); tool.name = toolElem.attribute(QStringLiteral("tool-name")); tool.version = toolElem.attribute(QStringLiteral("tool-version")); tool.company = toolElem.attribute(QStringLiteral("tool-company")); result.insert(tool.tool, tool); toolElem = toolElem.nextSiblingElement(QStringLiteral("tool")); } return result; } QStringList XliffStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("context-group")); while (!elem.isNull()) { if (elem.attribute(QStringLiteral("purpose")).contains(QLatin1String("location"))) { QDomElement context = elem.firstChildElement(QStringLiteral("context")); while (!context.isNull()) { QString sourcefile; QString linenumber; const QString contextType = context.attribute(QStringLiteral("context-type")); if (contextType == QLatin1String("sourcefile")) sourcefile = context.text(); else if (contextType == QLatin1String("linenumber")) linenumber = context.text(); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile % ':' % linenumber); context = context.nextSiblingElement(QStringLiteral("context")); } } elem = elem.nextSiblingElement(QStringLiteral("context-group")); } //qSort(result); return result; } static void initNoteFromElement(Note& note, QDomElement elem) { note.content = elem.text(); note.from = elem.attribute(QStringLiteral("from")); note.lang = elem.attribute(QStringLiteral("xml:lang")); if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("source")) note.annotates = Note::Source; else if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("target")) note.annotates = Note::Target; bool ok; note.priority = elem.attribute(QStringLiteral("priority")).toInt(&ok); if (!ok) note.priority = 0; } QVector XliffStorage::notes(const DocPosition& pos) const { QList result; QDomElement elem = entries.at(m_map.at(pos.entry)).firstChildElement(NOTE); while (!elem.isNull()) { Note note; initNoteFromElement(note, elem); result.append(note); elem = elem.nextSiblingElement(NOTE); } qSort(result); return result.toVector(); } QVector XliffStorage::developerNotes(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QVector(); } Note XliffStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< result; QDomNodeList notes = m_doc.elementsByTagName(NOTE); int i = notes.size(); while (--i >= 0) { QString from = notes.at(i).toElement().attribute(QStringLiteral("from")); if (!from.isEmpty()) result.insert(from); } return result.toList(); } QVector phaseNotes(QDomDocument m_doc, const QString& phasename, bool remove = false) { QVector result; QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); QDomElement noteElem = phaseElem.firstChildElement(NOTE); while (!noteElem.isNull()) { Note note; initNoteFromElement(note, noteElem); result.append(note); QDomElement old = noteElem; noteElem = noteElem.nextSiblingElement(NOTE); if (remove) phaseElem.removeChild(old); } return result; } QVector XliffStorage::phaseNotes(const QString& phasename) const { return ::phaseNotes(m_doc, phasename, false); } QVector XliffStorage::setPhaseNotes(const QString& phasename, QVector notes) { QVector result =::phaseNotes(m_doc, phasename, true); QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); foreach (const Note& note, notes) { QDomElement elem = phaseElem.appendChild(m_doc.createElement(NOTE)).toElement(); elem.appendChild(m_doc.createTextNode(note.content)); if (!note.from.isEmpty()) elem.setAttribute(QStringLiteral("from"), note.from); if (note.priority) elem.setAttribute(QStringLiteral("priority"), note.priority); } return result; } QString XliffStorage::setPhase(const DocPosition& pos, const QString& phase) { QString PHASENAME = QStringLiteral("phase-name"); targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); QString result = target.attribute(PHASENAME); if (phase.isEmpty()) target.removeAttribute(PHASENAME); else if (phase != result) target.setAttribute(PHASENAME, phase); return result; } QString XliffStorage::phase(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); return target.attribute(QStringLiteral("phase-name")); } QStringList XliffStorage::context(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QStringList(QString()); } QStringList XliffStorage::matchData(const DocPosition& pos) const { Q_UNUSED(pos); return QStringList(); } QString XliffStorage::id(const DocPosition& pos) const { return unitForPos(pos.entry).attribute(QStringLiteral("id")); } bool XliffStorage::isPlural(const DocPosition& pos) const { return m_plurals.contains(pos.entry); } /* bool XliffStorage::isApproved(const DocPosition& pos) const { return entries.at(m_map.at(pos.entry)).toElement().attribute("approved")=="yes"; } void XliffStorage::setApproved(const DocPosition& pos, bool approved) { static const char* const noyes[]={"no","yes"}; entries.at(m_map.at(pos.entry)).toElement().setAttribute("approved",noyes[approved]); } */ static const QString xliff_states[] = { U("new"), U("needs-translation"), U("needs-l10n"), U("needs-adaptation"), U("translated"), U("needs-review-translation"), U("needs-review-l10n"), U("needs-review-adaptation"), U("final"), U("signed-off") }; TargetState stringToState(const QString& state) { int i = sizeof(xliff_states) / sizeof(QString); while (--i > 0 && state != xliff_states[i]) ; return TargetState(i); } TargetState XliffStorage::setState(const DocPosition& pos, TargetState state) { targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); TargetState prev = stringToState(target.attribute(QStringLiteral("state"))); target.setAttribute(QStringLiteral("state"), xliff_states[state]); unitForPos(pos.entry).setAttribute(QStringLiteral("approved"), noyes[state == SignedOff]); return prev; } TargetState XliffStorage::state(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); if (!target.hasAttribute(QStringLiteral("state")) && unitForPos(pos.entry).attribute(QStringLiteral("approved")) == QLatin1String("yes")) return SignedOff; return stringToState(target.attribute(QStringLiteral("state"))); } bool XliffStorage::isEmpty(const DocPosition& pos) const { ContentEditingData data(ContentEditingData::CheckLength); return content(targetForPos(pos.entry), &data).isEmpty(); } bool XliffStorage::isEquivTrans(const DocPosition& pos) const { return targetForPos(pos.entry).attribute(QStringLiteral("equiv-trans")) != QLatin1String("no"); } void XliffStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { targetForPos(pos.entry).setAttribute(QStringLiteral("equiv-trans"), noyes[equivTrans]); } QDomElement XliffStorage::unitForPos(int pos) const { if (pos < size()) return entries.at(m_map.at(pos)).toElement(); return binEntries.at(pos - size()).toElement(); } QDomElement XliffStorage::targetForPos(int pos) const { return unitForPos(pos).firstChildElement(bintargettarget[pos < size()]); } QDomElement XliffStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(binsourcesource[pos < size()]); } int XliffStorage::binUnitsCount() const { return binEntries.size(); } int XliffStorage::unitById(const QString& id) const { return m_unitsById.contains(id) ? m_unitsById.value(id) : -1; } QString XliffStorage::originalOdfFilePath() { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.attribute(QStringLiteral("original")); } void XliffStorage::setOriginalOdfFilePath(const QString& odfFilePath) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.setAttribute(QStringLiteral("original"), odfFilePath); } //END STORAGE TRANSLATION diff --git a/src/catalog/xliff/xliffstorage.h b/src/catalog/xliff/xliffstorage.h index 731e43a..b59abcb 100644 --- a/src/catalog/xliff/xliffstorage.h +++ b/src/catalog/xliff/xliffstorage.h @@ -1,142 +1,143 @@ /* Copyright 2008-2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef XLIFFSTORAGE_H #define XLIFFSTORAGE_H #include "catalogstorage.h" #include #include #include #include #include // class QDomDocument; class XliffStorage: public CatalogStorage { public: XliffStorage(); ~XliffStorage() override; int capabilities() const override; int load(QIODevice* device) override; bool save(QIODevice* device, bool belongsToProject = false) override; int size() const override; bool isEmpty() const; //flat-model interface (ignores XLIFF grouping) QString source(const DocPosition& pos) const override; QString target(const DocPosition& pos) const override; QString sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; QString targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const override; CatalogString targetWithTags(DocPosition pos) const override; CatalogString sourceWithTags(DocPosition pos) const override; CatalogString catalogString(const DocPosition& pos) const override; /// all plural forms. pos.form doesn't matter TODO QStringList sourceAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } QStringList targetAllForms(const DocPosition& pos, bool stripNewLines = false) const override { Q_UNUSED(pos) Q_UNUSED(stripNewLines) return QStringList(); } void targetDelete(const DocPosition& pos, int count) override; void targetInsert(const DocPosition& pos, const QString& arg) override; void setTarget(const DocPosition& pos, const QString& arg) override;//called for mergeCatalog void targetInsertTag(const DocPosition&, const InlineTag&) override; InlineTag targetDeleteTag(const DocPosition&) override; Phase updatePhase(const Phase& phase) override; QList allPhases() const override; Phase phase(const QString& name) const override; QMap allTools() const override; QVector phaseNotes(const QString& phase) const override; QVector setPhaseNotes(const QString& phase, QVector notes) override; QStringList sourceFiles(const DocPosition& pos) const override; QVector altTrans(const DocPosition& pos) const override; ///@a pos.form is note number Note setNote(DocPosition pos, const Note& note) override; QVector notes(const DocPosition& pos) const override; QStringList noteAuthors() const override; QVector developerNotes(const DocPosition& pos) const override; QString setPhase(const DocPosition& pos, const QString& phase) override; QString phase(const DocPosition& pos) const override; QStringList context(const DocPosition& pos) const override; QStringList matchData(const DocPosition& pos) const override; QString id(const DocPosition& pos) const override; bool isPlural(const DocPosition& pos) const override; bool isEmpty(const DocPosition& pos) const override; bool isEquivTrans(const DocPosition& pos) const override; void setEquivTrans(const DocPosition& pos, bool equivTrans) override; TargetState state(const DocPosition& pos) const override; TargetState setState(const DocPosition& pos, TargetState state) override; int binUnitsCount() const override; int unitById(const QString& id) const override; QString mimetype() const override { return QStringLiteral("application/x-xliff"); } QString fileType() const override { return QStringLiteral("XLIFF (*.xliff *.xlf)"); } CatalogType type() const override { return Xliff; } QString originalOdfFilePath() override; void setOriginalOdfFilePath(const QString&) override; void setTargetLangCode(const QString& langCode) override; private: QDomElement unitForPos(int pos) const; QDomElement targetForPos(int pos) const; QDomElement sourceForPos(int pos) const; CatalogString catalogString(QDomElement unit, DocPosition::Part part) const; private: mutable QDomDocument m_doc; QVector m_map;//need mapping to treat plurals as 1 entry QSet m_plurals; QDomNodeList entries; QDomNodeList binEntries; QMap m_unitsById; }; #endif diff --git a/src/cataloglistview/cataloglistview.cpp b/src/cataloglistview/cataloglistview.cpp index 564647a..f1ce737 100644 --- a/src/cataloglistview/cataloglistview.cpp +++ b/src/cataloglistview/cataloglistview.cpp @@ -1,329 +1,330 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "cataloglistview.h" #include "lokalize_debug.h" #include "catalogmodel.h" #include "catalog.h" #include "project.h" #include "prefs.h" #include "headerviewmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class CatalogTreeView: public QTreeView { public: CatalogTreeView(QWidget * parent) : QTreeView(parent) {} ~CatalogTreeView() {} protected: void keyReleaseEvent(QKeyEvent *e) override { if (e->key() == Qt::Key_Return && currentIndex().isValid()) { emit clicked(currentIndex()); e->accept(); } else { QTreeView::keyReleaseEvent(e); } } }; CatalogView::CatalogView(QWidget* parent, Catalog* catalog) : QDockWidget(i18nc("@title:window aka Message Tree", "Translation Units"), parent) , m_browser(new CatalogTreeView(this)) , m_lineEdit(new QLineEdit(this)) , m_model(new CatalogTreeModel(this, catalog)) , m_proxyModel(new CatalogTreeFilterModel(this)) { setObjectName(QStringLiteral("catalogTreeView")); QWidget* w = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(w); layout->setContentsMargins(0, 0, 0, 0); QHBoxLayout* l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->setSpacing(0); layout->addLayout(l); m_lineEdit->setClearButtonEnabled(true); m_lineEdit->setPlaceholderText(i18n("Quick search...")); m_lineEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); connect(m_lineEdit, &QLineEdit::textChanged, this, &CatalogView::setFilterRegExp, Qt::QueuedConnection); // QShortcut* ctrlEsc=new QShortcut(QKeySequence(Qt::META+Qt::Key_Escape),this,SLOT(reset()),0,Qt::WidgetWithChildrenShortcut); QShortcut* esc = new QShortcut(QKeySequence(Qt::Key_Escape), this, 0, 0, Qt::WidgetWithChildrenShortcut); connect(esc, &QShortcut::activated, this, &CatalogView::escaped); QToolButton* btn = new QToolButton(w); btn->setPopupMode(QToolButton::InstantPopup); btn->setText(i18n("options")); //btn->setArrowType(Qt::DownArrow); btn->setMenu(new QMenu(this)); m_filterOptionsMenu = btn->menu(); connect(m_filterOptionsMenu, &QMenu::aboutToShow, this, &CatalogView::fillFilterOptionsMenu); connect(m_filterOptionsMenu, &QMenu::triggered, this, &CatalogView::filterOptionToggled); l->addWidget(m_lineEdit); l->addWidget(btn); layout->addWidget(m_browser); setTabOrder(m_lineEdit, btn); setTabOrder(btn, m_browser); setFocusProxy(m_lineEdit); setWidget(w); connect(m_browser, &CatalogTreeView::clicked, this, &CatalogView::slotItemActivated); m_browser->setRootIsDecorated(false); m_browser->setAllColumnsShowFocus(true); m_browser->setAlternatingRowColors(true); m_browser->viewport()->setBackgroundRole(QPalette::Background); #ifdef Q_OS_DARWIN QPalette p; p.setColor(QPalette::AlternateBase, p.color(QPalette::Background).darker(110)); p.setColor(QPalette::Highlight, p.color(QPalette::Background).darker(150)); m_browser->setPalette(p); #endif m_proxyModel->setSourceModel(m_model); m_browser->setModel(m_proxyModel); m_browser->setColumnWidth(0, m_browser->columnWidth(0) / 3); m_browser->setSortingEnabled(true); m_browser->sortByColumn(0, Qt::AscendingOrder); m_browser->setWordWrap(false); m_browser->setUniformRowHeights(true); m_browser->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); new HeaderViewMenuHandler(m_browser->header()); m_browser->header()->restoreState(readUiState("CatalogTreeViewState")); } CatalogView::~CatalogView() { writeUiState("CatalogTreeViewState", m_browser->header()->saveState()); } void CatalogView::setFocus() { QDockWidget::setFocus(); m_lineEdit->selectAll(); } void CatalogView::slotNewEntryDisplayed(const DocPosition& pos) { QModelIndex item = m_proxyModel->mapFromSource(m_model->index(pos.entry, 0)); m_browser->setCurrentIndex(item); m_browser->scrollTo(item/*,QAbstractItemView::PositionAtCenter*/); m_lastKnownDocPosition = pos.entry; } void CatalogView::setFilterRegExp() { QString expr = m_lineEdit->text(); if (m_proxyModel->filterRegExp().pattern() != expr) m_proxyModel->setFilterRegExp(m_proxyModel->filterOptions()&CatalogTreeFilterModel::IgnoreAccel ? expr.remove(Project::instance()->accel()) : expr); refreshCurrentIndex(); } void CatalogView::refreshCurrentIndex() { QModelIndex newPositionOfSelectedItem = m_proxyModel->mapFromSource(m_model->index(m_lastKnownDocPosition, 0)); m_browser->setCurrentIndex(newPositionOfSelectedItem); m_browser->scrollTo(newPositionOfSelectedItem); } void CatalogView::slotItemActivated(const QModelIndex& idx) { emit gotoEntry(DocPosition(m_proxyModel->mapToSource(idx).row()), 0); } void CatalogView::filterOptionToggled(QAction* action) { if (action->data().isNull()) return; int opt = action->data().toInt(); if (opt > 0) m_proxyModel->setFilterOptions(m_proxyModel->filterOptions()^opt); else { if (opt != -1) opt = -opt - 2; m_proxyModel->setFilterKeyColumn(opt); } m_filterOptionsMenu->clear(); refreshCurrentIndex(); } void CatalogView::fillFilterOptionsMenu() { m_filterOptionsMenu->clear(); if (m_proxyModel->individualRejectFilterEnabled()) m_filterOptionsMenu->addAction(i18n("Reset individual filter"), this, SLOT(setEntriesFilteredOut())); bool extStates = m_model->catalog()->capabilities()&ExtendedStates; const char* const basicTitles[] = { I18N_NOOP("Case insensitive"), I18N_NOOP("Ignore accelerator marks"), I18N_NOOP("Ready"), I18N_NOOP("Non-ready"), I18N_NOOP("Non-empty"), I18N_NOOP("Empty"), I18N_NOOP("Changed since file open"), I18N_NOOP("Unchanged since file open"), I18N_NOOP("Same in sync file"), I18N_NOOP("Different in sync file"), I18N_NOOP("Not in sync file"), I18N_NOOP("Plural"), I18N_NOOP("Non-plural"), }; const char* const* extTitles = Catalog::states(); const char* const* alltitles[2] = {basicTitles, extTitles}; QMenu* basicMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Basic")); QMenu* extMenu = extStates ? m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "States")) : 0; QMenu* allmenus[2] = {basicMenu, extMenu}; QMenu* columnsMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Searchable column")); QAction* txt; txt = m_filterOptionsMenu->addAction(i18nc("@title:inmenu", "Resort and refilter on content change"), m_proxyModel, &CatalogTreeFilterModel::setDynamicSortFilter); txt->setCheckable(true); txt->setChecked(m_proxyModel->dynamicSortFilter()); for (int i = 0; (1 << i) < CatalogTreeFilterModel::MaxOption; ++i) { bool ext = (1 << i) >= CatalogTreeFilterModel::New; if (!extStates && ext) break; txt = allmenus[ext]->addAction(i18n(alltitles[ext][i - ext * FIRSTSTATEPOSITION])); txt->setData(1 << i); txt->setCheckable(true); txt->setChecked(m_proxyModel->filterOptions() & (1 << i)); if ((1 << i) == CatalogTreeFilterModel::IgnoreAccel) basicMenu->addSeparator(); } if (!extStates) m_filterOptionsMenu->addSeparator(); for (int i = -1; i < CatalogTreeModel::DisplayedColumnCount; ++i) { qCWarning(LOKALIZE_LOG) << i; txt = columnsMenu->addAction((i == -1) ? i18nc("@item:inmenu all columns", "All") : m_model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString()); txt->setData(-i - 2); txt->setCheckable(true); txt->setChecked(m_proxyModel->filterKeyColumn() == i); } refreshCurrentIndex(); } void CatalogView::reset() { m_proxyModel->setFilterKeyColumn(-1); m_proxyModel->setFilterOptions(CatalogTreeFilterModel::AllStates); m_lineEdit->clear(); refreshCurrentIndex(); //emit gotoEntry(DocPosition(m_proxyModel->mapToSource(m_browser->currentIndex()).row()),0); slotItemActivated(m_browser->currentIndex()); } void CatalogView::setMergeCatalogPointer(MergeCatalog* pointer) { m_proxyModel->setMergeCatalogPointer(pointer); } int CatalogView::siblingEntryNumber(int step) { QModelIndex item = m_browser->currentIndex(); int lastRow = m_proxyModel->rowCount() - 1; if (!item.isValid()) { if (lastRow == -1) return -1; item = m_proxyModel->index((step == 1) ? 0 : lastRow, 0); m_browser->setCurrentIndex(item); } else { if (item.row() == ((step == -1) ? 0 : lastRow)) return -1; item = item.sibling(item.row() + step, 0); } return m_proxyModel->mapToSource(item).row(); } int CatalogView::nextEntryNumber() { return siblingEntryNumber(1); } int CatalogView::prevEntryNumber() { return siblingEntryNumber(-1); } static int edgeEntry(CatalogTreeFilterModel* m_proxyModel, int row) { if (!m_proxyModel->rowCount()) return -1; return m_proxyModel->mapToSource(m_proxyModel->index(row, 0)).row(); } int CatalogView::firstEntryNumber() { return edgeEntry(m_proxyModel, 0); } int CatalogView::lastEntryNumber() { return edgeEntry(m_proxyModel, m_proxyModel->rowCount() - 1); } void CatalogView::setEntryFilteredOut(int entry, bool filteredOut) { m_proxyModel->setEntryFilteredOut(entry, filteredOut); refreshCurrentIndex(); } void CatalogView::setEntriesFilteredOut(bool filteredOut) { show(); m_proxyModel->setEntriesFilteredOut(filteredOut); refreshCurrentIndex(); } diff --git a/src/cataloglistview/cataloglistview.h b/src/cataloglistview/cataloglistview.h index af2b3ed..76d3485 100644 --- a/src/cataloglistview/cataloglistview.h +++ b/src/cataloglistview/cataloglistview.h @@ -1,85 +1,86 @@ /* **************************************************************************** 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 CATALOGLISTVIEW_H #define CATALOGLISTVIEW_H #include "pos.h" #include "mergecatalog.h" #include class QTreeView; class CatalogTreeFilterModel; class CatalogTreeModel; class Catalog; class QLineEdit; class QMenu; class QAction; class QModelIndex; class CatalogTreeView; class CatalogView: public QDockWidget { Q_OBJECT public: explicit CatalogView(QWidget*, Catalog*); ~CatalogView(); void setEntryFilteredOut(int entry, bool filteredOut); int nextEntryNumber(); int prevEntryNumber(); int firstEntryNumber(); int lastEntryNumber(); private: int siblingEntryNumber(int step); void refreshCurrentIndex(); public slots: void slotNewEntryDisplayed(const DocPosition&); void setEntriesFilteredOut(bool filteredOut = false); void setFocus(); void reset(); void setMergeCatalogPointer(MergeCatalog* pointer); signals: void gotoEntry(const DocPosition&, int selection); void escaped(); private slots: void slotItemActivated(const QModelIndex&); void setFilterRegExp(); void fillFilterOptionsMenu(); void filterOptionToggled(QAction*); private: CatalogTreeView* m_browser; QLineEdit* m_lineEdit; QMenu* m_filterOptionsMenu; CatalogTreeModel* m_model; CatalogTreeFilterModel* m_proxyModel; int m_lastKnownDocPosition; }; #endif diff --git a/src/cataloglistview/catalogmodel.cpp b/src/cataloglistview/catalogmodel.cpp index 124760f..314f086 100644 --- a/src/cataloglistview/catalogmodel.cpp +++ b/src/cataloglistview/catalogmodel.cpp @@ -1,297 +1,298 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "catalogmodel.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include #include #include #include #include #define DYNAMICFILTER_LIMIT 256 QVector CatalogTreeModel::m_fonts; CatalogTreeModel::CatalogTreeModel(QObject* parent, Catalog* catalog) : QAbstractItemModel(parent) , m_catalog(catalog) , m_ignoreAccel(true) { if (m_fonts.isEmpty()) { QVector fonts(4, QApplication::font()); fonts[1].setItalic(true); //fuzzy fonts[2].setBold(true); //modified fonts[3].setItalic(true); //fuzzy fonts[3].setBold(true); //modified m_fonts.reserve(4); for (int i = 0; i < 4; i++) m_fonts << fonts.at(i); } connect(catalog, &Catalog::signalEntryModified, this, &CatalogTreeModel::reflectChanges); connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &CatalogTreeModel::fileLoaded); } QModelIndex CatalogTreeModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } QModelIndex CatalogTreeModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int CatalogTreeModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return DisplayedColumnCount; } void CatalogTreeModel::fileLoaded() { beginResetModel(); endResetModel(); } void CatalogTreeModel::reflectChanges(DocPosition pos) { emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); #if 0 I disabled dynamicSortFilter function //lazy sorting/filtering if (rowCount() < DYNAMICFILTER_LIMIT || m_prevChanged != pos) { qCWarning(LOKALIZE_LOG) << "first dataChanged emitment" << pos.entry; emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); if (!(rowCount() < DYNAMICFILTER_LIMIT)) { qCWarning(LOKALIZE_LOG) << "second dataChanged emitment" << m_prevChanged.entry; emit dataChanged(index(m_prevChanged.entry, 0), index(m_prevChanged.entry, DisplayedColumnCount - 1)); } } m_prevChanged = pos; #endif } int CatalogTreeModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_catalog->numberOfEntries(); } QVariant CatalogTreeModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case Key: return i18nc("@title:column", "Entry"); case Source: return i18nc("@title:column Original text", "Source"); case Target: return i18nc("@title:column Text in target language", "Target"); case Notes: return i18nc("@title:column", "Notes"); case Context: return i18nc("@title:column", "Context"); case TranslationStatus: return i18nc("@title:column", "Translation Status"); } return QVariant(); } QVariant CatalogTreeModel::data(const QModelIndex& index, int role) const { if (m_catalog->numberOfEntries() <= index.row()) return QVariant(); if (role == Qt::SizeHintRole) { //no need to cache because of uniform row heights return QFontMetrics(QApplication::font()).size(Qt::TextSingleLine, QString::fromLatin1(" ")); } else if (role == Qt::FontRole/* && index.column()==Target*/) { bool fuzzy = !m_catalog->isApproved(index.row()); bool modified = m_catalog->isModified(index.row()); return m_fonts.at(fuzzy * 1 | modified * 2); } else if (role == Qt::ForegroundRole) { if (m_catalog->isBookmarked(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::LinkText); } if (m_catalog->isObsolete(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::InactiveText); } } else if (role == Qt::UserRole) { switch (index.column()) { case TranslationStatus: return m_catalog->isApproved(index.row()); case IsEmpty: return m_catalog->isEmpty(index.row()); case State: return int(m_catalog->state(index.row())); case IsModified: return m_catalog->isModified(index.row()); case IsPlural: return m_catalog->isPlural(index.row()); default: role = Qt::DisplayRole; } } else if (role == StringFilterRole) { //exclude UI strings if (index.column() >= TranslationStatus) return QVariant(); else if (index.column() == Source || index.column() == Target) { QString str = index.column() == Source ? m_catalog->msgidWithPlurals(index.row(), false) : m_catalog->msgstrWithPlurals(index.row(), false); return m_ignoreAccel ? str.remove(Project::instance()->accel()) : str; } role = Qt::DisplayRole; } if (role != Qt::DisplayRole) return QVariant(); switch (index.column()) { case Key: return index.row() + 1; case Source: return m_catalog->msgidWithPlurals(index.row(), true); case Target: return m_catalog->msgstrWithPlurals(index.row(), true); case Notes: { QString result; foreach (const Note ¬e, m_catalog->notes(index.row())) result += note.content; return result; } case Context: return m_catalog->context(index.row()); case TranslationStatus: static QString statuses[] = {i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"), i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"), i18nc("@info:status", "Untranslated") }; if (m_catalog->isEmpty(index.row())) return statuses[2]; return statuses[!m_catalog->isApproved(index.row())]; } return QVariant(); } CatalogTreeFilterModel::CatalogTreeFilterModel(QObject* parent) : QSortFilterProxyModel(parent) , m_filterOptions(AllStates) , m_individualRejectFilterEnable(false) , m_mergeCatalog(NULL) { setFilterKeyColumn(-1); setFilterCaseSensitivity(Qt::CaseInsensitive); setFilterRole(CatalogTreeModel::StringFilterRole); setDynamicSortFilter(false); } void CatalogTreeFilterModel::setSourceModel(QAbstractItemModel* sourceModel) { QSortFilterProxyModel::setSourceModel(sourceModel); connect(sourceModel, &QAbstractItemModel::modelReset, this, QOverload<>::of(&CatalogTreeFilterModel::setEntriesFilteredOut)); setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut() { return setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut(bool filteredOut) { m_individualRejectFilter.fill(filteredOut, sourceModel()->rowCount()); m_individualRejectFilterEnable = filteredOut; invalidateFilter(); } void CatalogTreeFilterModel::setEntryFilteredOut(int entry, bool filteredOut) { // if (entry>=m_individualRejectFilter.size()) // sourceModelReset(); m_individualRejectFilter[entry] = filteredOut; m_individualRejectFilterEnable = true; invalidateFilter(); } void CatalogTreeFilterModel::setFilterOptions(int o) { m_filterOptions = o; setFilterCaseSensitivity(o & CaseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive); static_cast(sourceModel())->setIgnoreAccel(o & IgnoreAccel); invalidateFilter(); } bool CatalogTreeFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { int filerOptions = m_filterOptions; bool accepts = true; if (bool(filerOptions & Ready) != bool(filerOptions & NotReady)) { bool ready = sourceModel()->index(source_row, CatalogTreeModel::TranslationStatus, source_parent).data(Qt::UserRole).toBool(); accepts = (ready == bool(filerOptions & Ready) || ready != bool(filerOptions & NotReady)); } if (accepts && bool(filerOptions & NonEmpty) != bool(filerOptions & Empty)) { bool untr = sourceModel()->index(source_row, CatalogTreeModel::IsEmpty, source_parent).data(Qt::UserRole).toBool(); accepts = (untr == bool(filerOptions & Empty) || untr != bool(filerOptions & NonEmpty)); } if (accepts && bool(filerOptions & Modified) != bool(filerOptions & NonModified)) { bool modified = sourceModel()->index(source_row, CatalogTreeModel::IsModified, source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Modified) || modified != bool(filerOptions & NonModified)); } if (accepts && bool(filerOptions & Plural) != bool(filerOptions & NonPlural)) { bool modified = sourceModel()->index(source_row, CatalogTreeModel::IsPlural, source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Plural) || modified != bool(filerOptions & NonPlural)); } // These are the possible sync options of a row: // * SameInSync: The sync file contains a row with the same msgid and the same msgstr. // * DifferentInSync: The sync file contains a row with the same msgid and different msgstr. // * NotInSync: The sync file does not contain any row with the same msgid. // // The code below takes care of filtering rows when any of those options is not checked. // const int mask = (SameInSync | DifferentInSync | NotInSync); if (accepts && m_mergeCatalog && (filerOptions & mask) && (filerOptions & mask) != mask) { bool isPresent = m_mergeCatalog->isPresent(source_row); bool isDifferent = m_mergeCatalog->isDifferent(source_row); accepts = ! ((isPresent && !isDifferent && !bool(filerOptions & SameInSync)) || (isPresent && isDifferent && !bool(filerOptions & DifferentInSync)) || (!isPresent && !bool(filerOptions & NotInSync)) ); } if (accepts && (filerOptions & STATES) != STATES) { int state = sourceModel()->index(source_row, CatalogTreeModel::State, source_parent).data(Qt::UserRole).toInt(); accepts = (filerOptions & (1 << (state + FIRSTSTATEPOSITION))); } accepts = accepts && !(m_individualRejectFilterEnable && source_row < m_individualRejectFilter.size() && m_individualRejectFilter.at(source_row)); return accepts && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } void CatalogTreeFilterModel::setMergeCatalogPointer(MergeCatalog* pointer) { m_mergeCatalog = pointer; } diff --git a/src/cataloglistview/catalogmodel.h b/src/cataloglistview/catalogmodel.h index 0532219..a09647b 100644 --- a/src/cataloglistview/catalogmodel.h +++ b/src/cataloglistview/catalogmodel.h @@ -1,182 +1,183 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2013 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef CATALOGMODEL_H #define CATALOGMODEL_H #include "mergecatalog.h" #include "pos.h" #include #include #include class Catalog; /** * MVC wrapper for Catalog */ class CatalogTreeModel: public QAbstractItemModel { Q_OBJECT public: enum CatalogModelColumns { Key = 0, Source, Target, Notes, Context, TranslationStatus, IsEmpty, State, IsModified, IsPlural, ColumnCount, DisplayedColumnCount = TranslationStatus + 1 }; enum Roles { StringFilterRole = Qt::UserRole + 1 }; explicit CatalogTreeModel(QObject* parent, Catalog* catalog); ~CatalogTreeModel() override {} QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex())const override; QModelIndex parent(const QModelIndex&) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; Catalog* catalog()const { return m_catalog; } void setIgnoreAccel(bool n) { m_ignoreAccel = n; } public slots: void reflectChanges(DocPosition); void fileLoaded(); private: Catalog* m_catalog; bool m_ignoreAccel; static QVector m_fonts; //DocPos m_prevChanged; }; class CatalogTreeFilterModel: public QSortFilterProxyModel { Q_OBJECT public: enum FilterOptions { CaseInsensitive = 1 << 0, IgnoreAccel = 1 << 1, Ready = 1 << 2, NotReady = 1 << 3, NonEmpty = 1 << 4, Empty = 1 << 5, Modified = 1 << 6, NonModified = 1 << 7, SameInSync = 1 << 8, DifferentInSync = 1 << 9, NotInSync = 1 << 10, Plural = 1 << 11, NonPlural = 1 << 12, //states (see defines below) New = 1 << 13, NeedsTranslation = 1 << 14, NeedsL10n = 1 << 15, NeedsAdaptation = 1 << 16, Translated = 1 << 17, NeedsReviewTranslation = 1 << 18, NeedsReviewL10n = 1 << 19, NeedsReviewAdaptation = 1 << 20, Final = 1 << 21, SignedOff = 1 << 22, MaxOption = 1 << 23, AllStates = MaxOption - 1 }; #define STATES ((0xffff<<13)&(AllStates)) #define FIRSTSTATEPOSITION 13 explicit CatalogTreeFilterModel(QObject* parent); ~CatalogTreeFilterModel() {} bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; void setFilterOptions(int o); int filterOptions()const { return m_filterOptions; } void setSourceModel(QAbstractItemModel* sourceModel) override; bool individualRejectFilterEnabled() { return m_individualRejectFilterEnable; } void setEntryFilteredOut(int entry, bool filteredOut); void setMergeCatalogPointer(MergeCatalog* pointer); public slots: void setEntriesFilteredOut(); void setEntriesFilteredOut(bool filteredOut); void setDynamicSortFilter(bool enabled) { QSortFilterProxyModel::setDynamicSortFilter(enabled); } private: int m_filterOptions; bool m_individualRejectFilterEnable; QVector m_individualRejectFilter; //used from kross scripts MergeCatalog* m_mergeCatalog; }; #endif diff --git a/src/common/diff.cpp b/src/common/diff.cpp index 84d2454..ddc957d 100644 --- a/src/common/diff.cpp +++ b/src/common/diff.cpp @@ -1,435 +1,436 @@ /* ************************************************************************** This file is part of Lokalize wordDiff algorithm adoption and further refinement: Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets (based on Markus Stengel's GPL implementation of LCS-Delta algorithm as it is described in "Introduction to Algorithms", MIT Press, 2001, Second Edition, written by Thomas H. Cormen et. al. It uses dynamic programming to solve the Longest Common Subsequence (LCS) problem. - http://www.markusstengel.de/text/en/i_4_1_5_3.html) 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 "diff.h" #include "lokalize_debug.h" // #include "project.h" #include "prefs_lokalize.h" #include #include #include #include #include typedef enum { NOTHING = 0, ARROW_UP = 1, ARROW_LEFT = 2, ARROW_UP_LEFT = 3, FINAL = 4 } LCSMarker; static const QString addMarkerStart = QStringLiteral(""); static const QString addMarkerEnd = QStringLiteral(""); static const QString delMarkerStart = QStringLiteral(""); static const QString delMarkerEnd = QStringLiteral(""); QStringList calcLCS(const QStringList& s1Words, const QStringList& s2Words, const QStringList& s1Space, const QStringList& s2Space ); /** * The class is used for keeping "global" params of recursive function * * @short Class for keeping "global" params of recursive function * @author Nick Shaforostoff */ class LCSprinter { public: LCSprinter(const QStringList &s_1, const QStringList& s_2, QVector *b_, const uint nT_, uint index, const QStringList& s1Space_, const QStringList& s2Space_ ); ~LCSprinter() {} void printLCS(uint index); inline QStringList operator()(); private: QStringList s1, s2; QLinkedList resultString; QStringList s1Space, s2Space; QStringList::const_iterator it1, it2; QStringList::const_iterator it1Space, it2Space; uint nT: 31; //we're using 1d vector as 2d bool haveSpaces: 1; //"word: sfdfs" space is ": " QVector *b; //QStringList::iterator it1Space, it2Space; }; inline QStringList LCSprinter::operator()() { QStringList result; foreach (const QString& str, resultString) result << str; return result; } inline LCSprinter::LCSprinter(const QStringList& s_1, const QStringList& s_2, QVector *b_, const uint nT_, uint index, const QStringList& s1Space_, const QStringList& s2Space_ ) : s1(s_1) , s2(s_2) , s1Space(s1Space_) , s2Space(s2Space_) , it1(s1.constBegin()) , it2(s2.constBegin()) , it1Space(s1Space.constBegin()) , it2Space(s2Space.constBegin()) , nT(nT_) , b(b_) { haveSpaces = !s1Space_.isEmpty(); printLCS(index); } static QStringList prepareForInternalDiff(const QString& str) { QStringList result; int i = str.size(); while (--i >= 0) result.prepend(QString(str.at(i))); result.prepend(QString()); return result; } void LCSprinter::printLCS(uint index) { //fprintf(stderr,"%2d. %2d. %2d. %2d\n",(uint)(*b)[index],nT,index%nT, index); if (index % nT == 0 || index < nT) { // original LCS algo does not have to deal with ins before first common uint bound = index % nT; for (index = 0; index < bound; ++index) { resultString.append(addMarkerStart); resultString.append(*it2); ++it2; if (haveSpaces) { resultString.append(*it2Space); ++it2Space; } resultString.append(addMarkerEnd); } return; } if (ARROW_UP_LEFT == b->at(index)) { printLCS(index - nT - 1); if (it1 != s1.constEnd()) { //qCWarning(LOKALIZE_LOG) << "upleft '" << *it1 <<"'"; //qCWarning(LOKALIZE_LOG) << "upleft 1s" << *it1Space; //qCWarning(LOKALIZE_LOG) << "upleft 2s" << *it2Space; if (haveSpaces) { if ((*it1) == (*it2)) //case and accels resultString.append(*it1); else { QStringList word1 = prepareForInternalDiff(*it1); QStringList word2 = prepareForInternalDiff(*it2); QStringList empty; resultString.append(calcLCS(word1, word2, empty, empty).join(QString())); } if ((*it1Space) == (*it2Space)) resultString.append(*it1Space); else { QStringList word1 = prepareForInternalDiff(*it1Space); QStringList word2 = prepareForInternalDiff(*it2Space); QStringList empty; //empty=calcLCS(word1,word2,empty,empty); //???this is not really good if we use diff result in autosubst empty = calcLCS(word2, word1, empty, empty); empty.replaceInStrings(QStringLiteral("KBABELADD>"), QStringLiteral("KBABELTMP>")); empty.replaceInStrings(QStringLiteral("KBABELDEL>"), QStringLiteral("KBABELADD>")); empty.replaceInStrings(QStringLiteral("KBABELTMP>"), QStringLiteral("KBABELDEL>")); resultString.append(empty.join(QString())); } ++it1Space; ++it2Space; //qCWarning(LOKALIZE_LOG) << " common " << *it1; } else resultString.append(*it1);//we may guess that this is a batch job, i.e. TM search ++it1; ++it2; } } else if (ARROW_UP == b->at(index)) { printLCS(index - nT); // if (it1!=s1.end()) { //qCWarning(LOKALIZE_LOG)<<"APPENDDEL "<<*it1; //qCWarning(LOKALIZE_LOG)<<"APPENDDEL "<<*it1Space; resultString.append(delMarkerStart); resultString.append(*it1); ++it1; if (haveSpaces) { resultString.append(*it1Space); ++it1Space; } resultString.append(delMarkerEnd); } } else { printLCS(index - 1); resultString.append(addMarkerStart); resultString.append(*it2); ++it2; if (haveSpaces) { //qCWarning(LOKALIZE_LOG) << "add2 " << *it2; resultString.append(*it2Space); ++it2Space; } resultString.append(addMarkerEnd); } } // calculate the LCS QStringList calcLCS(const QStringList& s1Words, const QStringList& s2Words, const QStringList& s1Space, const QStringList& s2Space ) { uint i; uint j; uint mX = s1Words.count(); uint nY = s2Words.count(); //create lowered lists for matching, //and use original ones for printing (but only for non-batch) QStringList s1(s1Words); QStringList s2(s2Words); if (!s1Space.isEmpty()) { //accels are only removed by batch jobs //and this is not the one //also, lower things a bit :) for (i = 0; i < mX; ++i) s1[i] = s1.at(i).toLower(); for (i = 0; i < nY; ++i) s2[i] = s2.at(i).toLower(); #if 0 //i'm too lazy... QString accel(Project::instance()->accel()); i = mX; while (--i > 0) { if ((s1Space.at(i) == accel)) { s1[i] += s1[i + 1]; s1.removeAt(i + 1); s1Space.removeAt(i); s1Words[i] += s1[i + 1]; s1Words.removeAt(i + 1); --mX; --nY; } } #endif } uint mT = mX + 1; uint nT = nY + 1; QVector b(mT * nT, NOTHING); QVector c(mT * nT, 0); b[0] = FINAL; uint index_cache = 0; QStringList::const_iterator it1, it2; for (i = 1, it1 = s1.constBegin(); i < mT; ++i, ++it1) { for (j = 1, it2 = s2.constBegin(); j < nT; ++j, ++it2) { index_cache = i * nT + j; if ((*it1) == (*it2)) { c[index_cache] = c.at(index_cache - nT - 1) + 1; b[index_cache] = ARROW_UP_LEFT; } else if (c.at(index_cache - nT) >= c.at(index_cache - 1)) { c[index_cache] = c.at(index_cache - nT); b[index_cache] = ARROW_UP; } else { c[index_cache] = c.at(index_cache - 1); b[index_cache] = ARROW_LEFT; } } } c.clear(); LCSprinter printer(s1Words, s2Words, &b, nT, index_cache, s1Space, s2Space); return printer(); } QString wordDiff(QStringList s1, QStringList s2) { static const QString space(QStringLiteral(" ")); s1.prepend(space); s2.prepend(space); static QStringList empty; QStringList list = calcLCS(s1, s2, empty, empty); bool r = list.first() == space; if (r) list.removeFirst(); else qCDebug(LOKALIZE_LOG) << "first ' ' assumption is wrong" << list.first(); QString result = list.join(QString()); if (!r) result.remove(0, 1); result.remove(QStringLiteral("")); result.remove(QStringLiteral("")); return result; } //this also separates punctuation marks etc from words as _only_ they may have changed static void prepareLists(QString str, QStringList& main, QStringList& space, const QString& accel, QString markup) { Q_UNUSED(accel); int pos = 0; //accels are only removed by batch jobs //and this is not the one #if 0 QRegExp rxAccelInWord("[^\\W|\\d]" + accel + "[^\\W|\\d]"); int accelLen = accel.size(); while ((pos = rxAccelInWord.indexIn(str, pos)) != -1) { str.remove(rxAccelInWord.pos() + 1, accelLen); pos += 2; //two letters } #endif //QRegExp rxSplit("\\W+|\\d+"); //i tried that but it failed: if (!markup.isEmpty()) markup += '|'; QRegExp rxSplit('(' % markup % QLatin1String("\\W+|\\d+)+")); main = str.split(rxSplit, QString::SkipEmptyParts); main.prepend("\t");//little hack //ensure the string always begins with the space part str.prepend('\b'); pos = 0; while ((pos = rxSplit.indexIn(str, pos)) != -1) { space.append(rxSplit.cap(0)); pos += rxSplit.matchedLength(); } space.append(QString());//so we don't have to worry about list boundaries space.append(QString());//so we don't have to worry about list boundaries } QString userVisibleWordDiff(const QString& str1ForMatching, const QString& str2ForMatching, const QString& accel, const QString& markup, int options) { QStringList s1, s2; QStringList s1Space, s2Space; prepareLists(str1ForMatching, s1, s1Space, accel, markup); prepareLists(str2ForMatching, s2, s2Space, accel, markup); //QRegExp rxSpace("[^(\\W+|\\d+)]"); //i tried that but it failed: //QRegExp rxSpace("[^("+Project::instance()->markup()+"|\\W+|\\d+)]"); //QStringList s1Space(str1ForMatching.split(rxSpace,QString::SkipEmptyParts)); //QStringList s2Space(str2ForMatching.split(rxSpace,QString::SkipEmptyParts)); QStringList result(calcLCS(s1, s2, s1Space, s2Space)); result.removeFirst();//\t result.first().remove(0, 1); //\b // qCWarning(LOKALIZE_LOG)<<"wordDiff 1 '" <"), QString()); result.replaceInStrings(QStringLiteral(""), QString()); result.replaceInStrings(QStringLiteral(""), QStringLiteral("{KBABELADD}")); result.replaceInStrings(QStringLiteral(""), QStringLiteral("{/KBABELADD}")); result.replaceInStrings(QStringLiteral(""), QStringLiteral("{KBABELDEL}")); result.replaceInStrings(QStringLiteral(""), QStringLiteral("{/KBABELDEL}")); if (options & Html) { result.replaceInStrings(QStringLiteral("&"), QStringLiteral("&")); result.replaceInStrings(QStringLiteral("<"), QStringLiteral("<")); result.replaceInStrings(QStringLiteral(">"), QStringLiteral(">")); } //result.last().chop(1);//\b //qCWarning(LOKALIZE_LOG)<<"DIFF RESULT '" <")); res.replace(QLatin1String("{/KBABELADD}"), QLatin1String("")); res.replace(QLatin1String("{KBABELDEL}"), QLatin1String("")); res.replace(QLatin1String("{/KBABELDEL}"), QLatin1String("")); res.replace(QLatin1String("\\n"), QLatin1String("\\n
")); } return res; } diff --git a/src/common/diff.h b/src/common/diff.h index 997aa97..49de119 100644 --- a/src/common/diff.h +++ b/src/common/diff.h @@ -1,63 +1,64 @@ /* ************************************************************************** This file is part of Lokalize wordDiff algorithm adoption and further refinement: Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets (based on Markus Stengel's GPL implementation of LCS-Delta algorithm as it is described in "Introduction to Algorithms", MIT Press, 2001, Second Edition, written by Thomas H. Cormen et. al. It uses dynamic programming to solve the Longest Common Subsequence (LCS) problem. - http://www.markusstengel.de/text/en/i_4_1_5_3.html) 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 DIFF_H #define DIFF_H #include /** * @short Word-by-word diff algorithm * * Word-by-word diff algorithm * * Based on Markus Stengel's GPLv2+ implementation of LCS-Delta algorithm * as it is described in "Introduction to Algorithms", MIT Press, 2001, Second Edition, written by Thomas H. Cormen et. al. * It uses dynamic programming to solve the Longest Common Subsequence (LCS) problem. * http://www.markusstengel.de/text/en/i_4_1_5_3.html) * * This is high-level wrapper * * @author Nick Shaforostoff */ enum {Html = 1}; QString userVisibleWordDiff(const QString& oldString, const QString& newString, const QString& accelRx, const QString& markupRx, int options = 0); /** * This is low-level wrapper used for evaluating translation memory search results * * You have to explicitly prepend lists with identical strings */ QString wordDiff(QStringList s1, QStringList s2); #endif // DIFF_H diff --git a/src/common/domroutines.cpp b/src/common/domroutines.cpp index 5438049..bc65d73 100644 --- a/src/common/domroutines.cpp +++ b/src/common/domroutines.cpp @@ -1,39 +1,40 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "domroutines.h" void setText(QDomElement element, QString text) { QDomNodeList children = element.childNodes(); for (int i = 0; i < children.count(); i++) { if (children.at(i).isCharacterData()) { children.at(i).toCharacterData().setData(text); text.clear(); } } if (!text.isEmpty()) element.appendChild(element.ownerDocument().createTextNode(text)); } diff --git a/src/common/domroutines.h b/src/common/domroutines.h index bdbd4eb..430521f 100644 --- a/src/common/domroutines.h +++ b/src/common/domroutines.h @@ -1,30 +1,31 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef DOMROUTINES_H #define DOMROUTINES_H #include void setText(QDomElement element, QString text); #endif diff --git a/src/common/fastsizehintitemdelegate.cpp b/src/common/fastsizehintitemdelegate.cpp index 5f9b6bc..42841cf 100644 --- a/src/common/fastsizehintitemdelegate.cpp +++ b/src/common/fastsizehintitemdelegate.cpp @@ -1,134 +1,135 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2012 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 "fastsizehintitemdelegate.h" #include #include #include #include FastSizeHintItemDelegate::FastSizeHintItemDelegate(QObject *parent, const QVector& slc, const QVector& rtc) : QItemDelegate(parent) , singleLineColumns(slc) , richTextColumns(rtc) , activeScheme(QPalette::Active, KColorScheme::View) {} void FastSizeHintItemDelegate::reset() { cache.clear(); } QSize FastSizeHintItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { int lineCount = 1; int nPos = 20; int column = qMax(index.column(), 0); if (!singleLineColumns.at(column)) { QString text = index.data().toString(); 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 FastSizeHintItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { painter->save(); painter->setClipping(true); painter->setClipRect(option.rect); QBrush bgBrush; const KColorScheme& scheme = activeScheme; if (option.state & QStyle::State_MouseOver) bgBrush = scheme.background(KColorScheme::LinkBackground); else if (index.row() % 2) bgBrush = scheme.background(KColorScheme::AlternateBackground); else bgBrush = scheme.background(KColorScheme::NormalBackground); painter->fillRect(option.rect, bgBrush); painter->setClipRect(option.rect.adjusted(0, 0, -2, 0)); //painter->setFont(option.font); RowColumnUnion rc; rc.index.row = index.row(); rc.index.column = index.column(); //TMDBModel* m=static_cast(index.model()); if (!cache.contains(rc.v)) { QString text = index.data(FastSizeHintItemDelegate::HtmlDisplayRole).toString(); cache.insert(rc.v, new QStaticText(text)); QTextOption textOption = cache.object(rc.v)->textOption(); textOption.setWrapMode(QTextOption::NoWrap); cache.object(rc.v)->setTextOption(textOption); cache.object(rc.v)->setTextFormat(richTextColumns.at(index.column()) ? Qt::RichText : Qt::PlainText); } int rectWidth = option.rect.width(); QStaticText* staticText = cache.object(rc.v); //staticText->setTextWidth(rectWidth-4); QPoint textStartPoint = option.rect.topLeft(); textStartPoint.rx() += 2; painter->drawStaticText(textStartPoint, *staticText); if (staticText->size().width() <= rectWidth - 4) { painter->restore(); return; } painter->setPen(bgBrush.color()); QPoint p1 = option.rect.topRight(); QPoint p2 = option.rect.bottomRight(); int limit = qMin(8, rectWidth - 2); int i = limit; while (--i > 0) { painter->setOpacity(float(i) / limit); painter->drawLine(p1, p2); p1.rx()--; p2.rx()--; } painter->restore(); } QString convertToHtml(QString str, bool italics) { /* if (str.isEmpty()) return str; */ str = Qt::convertFromPlainText(str); //FIXME use another routine (this has bugs) if (italics) str = "

" % QString::fromRawData(str.unicode() + 3, str.length() - 3 - 4) % "

"; return str; } diff --git a/src/common/fastsizehintitemdelegate.h b/src/common/fastsizehintitemdelegate.h index 18f2fc7..6d5fec6 100644 --- a/src/common/fastsizehintitemdelegate.h +++ b/src/common/fastsizehintitemdelegate.h @@ -1,77 +1,78 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2012 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 FASTSIZEHINTITEMDELEGATE_H #define FASTSIZEHINTITEMDELEGATE_H #include #include #include #include QString convertToHtml(QString string, bool italics = false); /** * remember to connect appropriate signals to reset slot * for delegate to have actual cache * * @author Nick Shaforostoff */ class FastSizeHintItemDelegate: public QItemDelegate { Q_OBJECT public: enum Roles { HtmlDisplayRole = Qt::UserRole + 5 }; explicit FastSizeHintItemDelegate(QObject *parent, const QVector& slc, const QVector& rtc); ~FastSizeHintItemDelegate() override {} void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; public slots: void reset(); private: QVector singleLineColumns; QVector richTextColumns; struct RowColumn { short row: 16; short column: 16; }; union RowColumnUnion { RowColumn index; int v; }; mutable QCache cache; KColorScheme activeScheme; }; #endif // FASTSIZEHINTITEMDELEGATE_H diff --git a/src/common/flowlayout.cpp b/src/common/flowlayout.cpp index acf29a8..53550c3 100644 --- a/src/common/flowlayout.cpp +++ b/src/common/flowlayout.cpp @@ -1,204 +1,205 @@ /* **************************************************************************** This file is part of KAider Copyright (C) 2007 by Nick Shaforostoff Copyright (C) 2004-2007 Trolltech ASA. All rights reserved. + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "flowlayout.h" #include "lokalize_debug.h" #include "termlabel.h" #include "glossaryview.h" #include using namespace GlossaryNS; FlowLayout::FlowLayout(User user, QWidget *signalingWidget, const QVector& actions, int margin, int spacing) : QLayout() , m_index(0) , m_receiver(signalingWidget) { setSizeConstraint(QLayout::SetMinAndMaxSize); setMargin(margin); setSpacing(spacing); if (user == glossary) { foreach (QAction* action, actions) { TermLabel* label = new TermLabel(action); /*this,m_keys.at(count())*/ connect(action, &QAction::triggered, label, &GlossaryNS::TermLabel::insert); connect(label, &GlossaryNS::TermLabel::insertTerm, (GlossaryNS::GlossaryView*)m_receiver, &GlossaryNS::GlossaryView::termInsertRequested); label->hide(); addWidget(label); } } // if (m_keys.isEmpty()) // { // // Qt::Key key=Qt::Key_A; // // for (;key<=Qt::Key_Z;++key) // // { // // if (KGlobalAccel::findActionNameSystemwide(Qt::ALT+key).isEmpty()) // // { // // keys.append(key); // // } // // } // int i=(int)Qt::Key_A; // for (;i<=(int)Qt::Key_Z;++i) // { // if (KGlobalAccel::findActionNameSystemwide(Qt::ALT+Qt::CTRL+(Qt::Key)i).isEmpty()) // { // m_keys.append((Qt::Key)i); // } // } // // } } FlowLayout::~FlowLayout() { QLayoutItem *item; while ((item = takeAt(0))) delete item; } QLayoutItem *FlowLayout::takeAt(int index) { if (index >= 0 && index < itemList.size()) return itemList.takeAt(index); else return 0; } QLayoutItem *FlowLayout::itemAt(int index) const { return itemList.value(index); } void FlowLayout::addItem(QLayoutItem *item) { itemList.append(item); } int FlowLayout::count() const { return itemList.size(); } Qt::Orientations FlowLayout::expandingDirections() const { return 0; } bool FlowLayout::hasHeightForWidth() const { return true; } int FlowLayout::heightForWidth(int width) const { int height = doLayout(QRect(0, 0, width, 0), true); return height; } void FlowLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); doLayout(rect, false); } QSize FlowLayout::sizeHint() const { return minimumSize(); } QSize FlowLayout::minimumSize() const { QSize size; foreach (QLayoutItem* item, itemList) size = size.expandedTo(item->minimumSize()); size += QSize(2 * margin(), 2 * margin()); return size; } int FlowLayout::doLayout(const QRect &rect, bool testOnly) const { int x = rect.x(); int y = rect.y(); int lineHeight = 0; foreach (QLayoutItem* item, itemList) { int nextX = x + item->sizeHint().width() + spacing(); if (nextX - spacing() > rect.right() && lineHeight > 0) { x = rect.x(); y = y + lineHeight + spacing(); nextX = x + item->sizeHint().width() + spacing(); lineHeight = 0; } if (!testOnly) item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); x = nextX; lineHeight = qMax(lineHeight, item->sizeHint().height()); } return y + lineHeight - rect.y(); } void FlowLayout::clearTerms() { setEnabled(false); foreach (QLayoutItem* item, itemList) static_cast(item->widget())->hide(); m_index = 0; setEnabled(true); } void FlowLayout::addTerm(const QString& term, const QByteArray& entryId, bool capFirst) { //fill layout with labels while (m_index >= count()) { TermLabel* label = new TermLabel; connect(label, &TermLabel::insertTerm, (GlossaryNS::GlossaryView*)m_receiver, &GlossaryNS::GlossaryView::termInsertRequested); addWidget(label); } TermLabel* label = static_cast(itemAt(m_index)->widget()); label->setText(term, entryId, capFirst); label->show(); ++m_index; } diff --git a/src/common/flowlayout.h b/src/common/flowlayout.h index 22a251c..875340c 100644 --- a/src/common/flowlayout.h +++ b/src/common/flowlayout.h @@ -1,93 +1,94 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007 by Nick Shaforostoff Copyright (C) 2004-2007 Trolltech ASA. All rights reserved. + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef FLOWLAYOUT_H #define FLOWLAYOUT_H #include "glossary.h" #include #include class QAction; /** * used in glossary and kross views * * copied from 'pretty' docs */ class FlowLayout: public QLayout { public: enum User { glossary, webquery, standard }; /** * c'tor for glossary view */ explicit FlowLayout(User user = standard, QWidget *signalingWidget = nullptr, const QVector& actions = QVector(), int margin = 0, int spacing = -1); ~FlowLayout() override; void addItem(QLayoutItem *item) override; Qt::Orientations expandingDirections() const override; bool hasHeightForWidth() const override; int heightForWidth(int) const override; int count() const override; QLayoutItem *itemAt(int index) const override; QSize minimumSize() const override; void setGeometry(const QRect &rect) override; QSize sizeHint() const override; QLayoutItem *takeAt(int index) override; /** * @param term is the term matched * @param entryId is index of entry in the Glossary list * @param capFirst whether the first letter should be capitalized */ void addTerm(const QString& term, const QByteArray& entryId, bool capFirst = false); void clearTerms(); private: int doLayout(const QRect &rect, bool testOnly) const; QList itemList; int m_index; //of the nearest free label ; or the next index of btn QWidget *m_receiver; }; #endif diff --git a/src/common/headerviewmenu.cpp b/src/common/headerviewmenu.cpp index 15a374d..351407b 100644 --- a/src/common/headerviewmenu.cpp +++ b/src/common/headerviewmenu.cpp @@ -1,56 +1,57 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 "headerviewmenu.h" #include #include HeaderViewMenuHandler::HeaderViewMenuHandler(QHeaderView* headerView): QObject(headerView) { headerView->setContextMenuPolicy(Qt::CustomContextMenu); connect(headerView, &QHeaderView::customContextMenuRequested, this, &HeaderViewMenuHandler::headerMenuRequested); } void HeaderViewMenuHandler::headerMenuRequested(QPoint pos) { QHeaderView* headerView = static_cast(parent()); bool allowHiding = (headerView->count() - headerView->hiddenSectionCount()) > 1; QMenu* headerMenu = new QMenu(headerView); connect(headerMenu, &QMenu::aboutToHide, headerMenu, &QMenu::deleteLater, Qt::QueuedConnection); connect(headerMenu, &QMenu::triggered, this, &HeaderViewMenuHandler::headerMenuActionToggled); for (int i = 0; i < headerView->count(); ++i) { QAction* a = headerMenu->addAction(headerView->model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString()); a->setData(i); a->setCheckable(true); a->setChecked(!headerView->isSectionHidden(i)); a->setEnabled(headerView->isSectionHidden(i) || allowHiding); } headerMenu->popup(headerView->mapToGlobal(pos)); } void HeaderViewMenuHandler::headerMenuActionToggled(QAction* a) { QHeaderView* headerView = static_cast(parent()); headerView->setSectionHidden(a->data().toInt(), !a->isChecked()); } diff --git a/src/common/headerviewmenu.h b/src/common/headerviewmenu.h index 2d351cf..ff5e6a7 100644 --- a/src/common/headerviewmenu.h +++ b/src/common/headerviewmenu.h @@ -1,42 +1,43 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 . **************************************************************************** */ #ifndef HEADERVIEWMENU_H #define HEADERVIEWMENU_H #include #include #include class QAction; class HeaderViewMenuHandler: public QObject { Q_OBJECT public: explicit HeaderViewMenuHandler(QHeaderView* parent); private slots: void headerMenuRequested(QPoint); void headerMenuActionToggled(QAction*); }; #endif diff --git a/src/common/htmlhelpers.cpp b/src/common/htmlhelpers.cpp index 0602545..048698b 100644 --- a/src/common/htmlhelpers.cpp +++ b/src/common/htmlhelpers.cpp @@ -1,44 +1,45 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 #include #include QString escapeWithLinks(const QString& text) { QString html; static QRegularExpression urlDetector(QStringLiteral("(https?|ftp)://[^\\s/$.?#].[^\\s]*")); QStringList parts = text.split(urlDetector); if (parts.size()) html += parts.takeFirst().toHtmlEscaped(); QRegularExpressionMatchIterator i = urlDetector.globalMatch(text); while (i.hasNext()) { QRegularExpressionMatch match = i.next(); QString word = match.captured(0); html += QStringLiteral("") % word.toHtmlEscaped() % QStringLiteral(""); if (parts.size()) html += parts.takeFirst().toHtmlEscaped(); } return html; } diff --git a/src/common/languagelistmodel.cpp b/src/common/languagelistmodel.cpp index 8fd12f7..776acaa 100644 --- a/src/common/languagelistmodel.cpp +++ b/src/common/languagelistmodel.cpp @@ -1,167 +1,168 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-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 "languagelistmodel.h" #include #include #include #include #include #include #include #include #include LanguageListModel* LanguageListModel::_instance = 0; LanguageListModel* LanguageListModel::_emptyLangInstance = 0; void LanguageListModel::cleanupLanguageListModel() { delete LanguageListModel::_instance; LanguageListModel::_instance = 0; delete LanguageListModel::_emptyLangInstance; LanguageListModel::_emptyLangInstance = 0; } LanguageListModel* LanguageListModel::instance() { if (_instance == 0) { _instance = new LanguageListModel(); qAddPostRoutine(LanguageListModel::cleanupLanguageListModel); } return _instance; } LanguageListModel* LanguageListModel::emptyLangInstance() { if (_emptyLangInstance == 0) _emptyLangInstance = new LanguageListModel(WithEmptyLang); return _emptyLangInstance; } LanguageListModel::LanguageListModel(ModelType type, QObject* parent) : QStringListModel(parent) , m_sortModel(new QSortFilterProxyModel(this)) , m_systemLangList(new KConfig(QLatin1String("locale/kf5_all_languages"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation)) { setStringList(m_systemLangList->groupList()); if (type == WithEmptyLang) insertRows(rowCount(), 1); #if 0 //KDE5PORT KIconLoader::global()->addExtraDesktopThemes(); #endif //qCWarning(LOKALIZE_LOG)<hasContext(KIconLoader::International); //qCDebug(LOKALIZE_LOG)<queryIconsByContext(KIconLoader::NoGroup,KIconLoader::International); m_sortModel->setSourceModel(this); m_sortModel->sort(0); } QVariant LanguageListModel::data(const QModelIndex& index, int role) const { if (role == Qt::DecorationRole) { #if 0 //# static QMap iconCache; QString langCode = stringList().at(index.row()); if (!iconCache.contains(langCode)) { QString code = QLocale(langCode).name(); QString path; if (code.contains('_')) code = QString::fromRawData(code.unicode() + 3, 2).toLower(); if (code != "C") { static const QString flagPath("l10n/%1/flag.png"); path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("locale/") + flagPath.arg(code)); } iconCache[langCode] = QIcon(path); } return iconCache.value(langCode); #endif } else if (role == Qt::DisplayRole) { const QString& code = stringList().at(index.row()); if (code.isEmpty()) return code; //qCDebug(LOKALIZE_LOG)<<"languageCodeToName"< displayNames(stringList().size()); if (displayNames.at(index.row()).length()) return displayNames.at(index.row()); return QVariant::fromValue( displayNames[index.row()] = KConfigGroup(m_systemLangList, code).readEntry("Name") % QStringLiteral(" (") % code % ')'); } return QStringListModel::data(index, role); } QFlags< Qt::ItemFlag > LanguageListModel::flags(const QModelIndex& index) const { return QStringListModel::flags(index); } int LanguageListModel::sortModelRowForLangCode(const QString& langCode) { return m_sortModel->mapFromSource(index(stringList().indexOf(langCode))).row(); } QString LanguageListModel::langCodeForSortModelRow(int row) { return stringList().at(m_sortModel->mapToSource(m_sortModel->index(row, 0)).row()); } #include "prefs.h" #include "project.h" #include #include #include #include #include #include QString getTargetLangCode(const QString& title, bool askUser) { if (!askUser) { if (Project::instance()->targetLangCode().length()) return Project::instance()->targetLangCode(); return QLocale::system().name(); } QDialog dlg(SettingsController::instance()->mainWindowPtr()); dlg.setWindowTitle(title); QHBoxLayout* l = new QHBoxLayout(&dlg); l->addWidget(new QLabel(i18n("Target language:"), &dlg)); QComboBox* lc = new QComboBox(&dlg); l->addWidget(lc); lc->setModel(LanguageListModel::instance()->sortModel()); lc->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(Project::instance()->targetLangCode())); QDialogButtonBox* btn = new QDialogButtonBox(QDialogButtonBox::Ok, &dlg); l->addWidget(btn); QObject::connect(btn, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); QObject::connect(btn, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); dlg.show(); dlg.activateWindow(); //if we're called from another app if (!dlg.exec()) return Project::instance()->targetLangCode(); return LanguageListModel::instance()->langCodeForSortModelRow(lc->currentIndex()); } diff --git a/src/common/languagelistmodel.h b/src/common/languagelistmodel.h index 982f129..6027108 100644 --- a/src/common/languagelistmodel.h +++ b/src/common/languagelistmodel.h @@ -1,65 +1,66 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 LANGUAGELISTMODEL_H #define LANGUAGELISTMODEL_H #include class QSortFilterProxyModel; class KConfig; class LanguageListModel: public QStringListModel { enum ModelType { Default, WithEmptyLang }; public: static LanguageListModel* instance(); static LanguageListModel* emptyLangInstance(); private: static LanguageListModel * _instance; static LanguageListModel * _emptyLangInstance; static void cleanupLanguageListModel(); LanguageListModel(ModelType type = Default, QObject* parent = nullptr); QSortFilterProxyModel* m_sortModel; KConfig* m_systemLangList; public: QVariant data(const QModelIndex& index, int role) const override; QFlags< Qt::ItemFlag > flags(const QModelIndex& index) const override; QSortFilterProxyModel* sortModel() const { return m_sortModel; } int sortModelRowForLangCode(const QString&); QString langCodeForSortModelRow(int); }; QString getTargetLangCode(const QString& title, bool askUser = false); #endif // LANGUAGELISTMODEL_H diff --git a/src/common/stemming.cpp b/src/common/stemming.cpp index 2c531b8..8b4a511 100644 --- a/src/common/stemming.cpp +++ b/src/common/stemming.cpp @@ -1,124 +1,125 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "stemming.h" #include "lokalize_debug.h" #include #include #include #include #include QString enhanceLangCode(const QString& langCode) { if (langCode.length() != 2) return langCode; return QLocale(langCode).name(); } #ifdef HAVE_HUNSPELL #include #include struct SpellerAndCodec { Hunspell* speller; QTextCodec* codec; SpellerAndCodec(): speller(0), codec(0) {} SpellerAndCodec(const QString& langCode); }; SpellerAndCodec::SpellerAndCodec(const QString& langCode) : speller(0), codec(0) { #ifdef Q_OS_MAC QString dictPath = QStringLiteral("/Applications/LibreOffice.app/Contents/Resources/extensions/dict-") % langCode.leftRef(2) % '/'; if (langCode == QLatin1String("pl_PL")) dictPath = QStringLiteral("/System/Library/Spelling/"); #elif defined(Q_OS_WIN) QString dictPath = QStringLiteral("C:/Program Files (x86)/LibreOffice 5/share/extensions/dict-") % langCode.leftRef(2) % '/'; #else QString dictPath = QStringLiteral("/usr/share/hunspell/"); if (!QFileInfo::exists(dictPath)) dictPath = QStringLiteral("/usr/share/myspell/"); #endif QString dic = dictPath % langCode % QLatin1String(".dic"); if (!QFileInfo::exists(dic)) dic = dictPath % enhanceLangCode(langCode) % QLatin1String(".dic"); if (QFileInfo::exists(dic)) { speller = new Hunspell(QString(dictPath % langCode % ".aff").toLatin1().constData(), dic.toLatin1().constData()); codec = QTextCodec::codecForName(speller->get_dic_encoding()); if (!codec) codec = QTextCodec::codecForLocale(); } } static QMap hunspellers; #endif QString stem(const QString& langCode, const QString& word) { QString result = word; #ifdef HAVE_HUNSPELL static QMutex mutex; QMutexLocker locker(&mutex); if (!hunspellers.contains(langCode)) { hunspellers.insert(langCode, SpellerAndCodec(langCode)); } SpellerAndCodec sc(hunspellers.value(langCode)); Hunspell* speller = sc.speller; if (!speller) return word; char** result1; char** result2; int n1 = speller->analyze(&result1, sc.codec->fromUnicode(word)); int n2 = speller->stem(&result2, result1, n1); if (n2) result = sc.codec->toUnicode(result2[0]); speller->free_list(&result1, n1); speller->free_list(&result2, n2); #endif return result; } void cleanupSpellers() { #ifdef HAVE_HUNSPELL foreach (const SpellerAndCodec& sc, hunspellers) delete sc.speller; #endif } diff --git a/src/common/stemming.h b/src/common/stemming.h index 5067033..4efe885 100644 --- a/src/common/stemming.h +++ b/src/common/stemming.h @@ -1,33 +1,34 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 STEMMING_H #define STEMMING_H #include QString enhanceLangCode(const QString& langCode); //it -> it_IT QString stem(const QString& langCode, const QString& word); void cleanupSpellers(); #endif diff --git a/src/common/termlabel.cpp b/src/common/termlabel.cpp index d01f952..92febc5 100644 --- a/src/common/termlabel.cpp +++ b/src/common/termlabel.cpp @@ -1,141 +1,142 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) 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 "termlabel.h" #include "lokalize_debug.h" #include "glossarywindow.h" #include #include #include #include using namespace GlossaryNS; //#include // TermLabel::TermLabel(QAction* action/*const QString& shortcutQWidget* parent,Qt::Key key,const QString& termTransl*/) // : m_action(action) // //: m_shortcut(shortcut) // // : QLabel(/*parent*/) // //, m_termTransl(termTransl) // { // // setFlat(true); // // grabShortcut(Qt::ALT+Qt::CTRL+key); // // qCWarning(LOKALIZE_LOG) << "dsds " << grabShortcut(Qt::ALT+key); // } // //~TermLabel(){} // // bool TermLabel::event(QEvent *event) // // { // // if (event->type() != QEvent::Shortcut) // // return QLabel::event(event); // // // // // qCWarning(LOKALIZE_LOG) << "dsds " << m_termTransl; // // emit insertTerm(m_termTransl); // // return true; // // } void TermLabel::insert() { GlossaryNS::Glossary* glossary = Project::instance()->glossary(); if (m_entryId.isEmpty()) return; QString termTrans; const QStringList& termTarget = glossary->terms(m_entryId, Project::instance()->targetLangCode()); if (termTarget.count() > 1) { QMenu menu; int limit = termTarget.count(); menu.setActiveAction(menu.addAction(termTarget.at(0))); int i = 1; for (; i < limit; ++i) menu.addAction(termTarget.at(i)); QAction* txt = menu.exec(mapToGlobal(QPoint(0, 0))); if (!txt) return; termTrans = txt->text(); } else if (termTarget.count() == 1) termTrans = termTarget.first(); if (m_capFirst && !termTrans.isEmpty()) termTrans[0] = termTrans.at(0).toUpper(); emit insertTerm(termTrans); } void TermLabel::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::RightButton) { QMenu menu; menu.addAction(i18nc("@action:inmenu Edit term", "Edit")); QAction* txt = menu.exec(event->globalPos()); if (txt) { GlossaryNS::GlossaryWindow* glossaryWindow = Project::instance()->showGlossary(); if (glossaryWindow) glossaryWindow->selectEntry(m_entryId); } } else insert(); } void TermLabel::setText(const QString& term, const QByteArray& entryId, bool capFirst) { m_entryId = entryId; m_capFirst = capFirst; static const QString n = QStringLiteral(" \n "); QLabel::setText(QString(term + QString(m_action ? QString(QStringLiteral(" [") % m_action->shortcut().toString(QKeySequence::NativeText) % QStringLiteral("] \n ")) : n) //m_shortcut % Project::instance()->glossary()->terms(m_entryId, Project::instance()->targetLangCode()).join(n) % n)); } #if 0 void QueryResultBtn::insert() { // qCWarning(LOKALIZE_LOG)<<"ins "< + 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 TERMLABEL_H #define TERMLABEL_H #include #include "glossary.h" #include "project.h" namespace GlossaryNS { /** * flowlayout item */ class TermLabel: public QLabel//QPushButton { Q_OBJECT public: explicit TermLabel(QAction* a = nullptr): m_capFirst(false), m_action(a) {} ~TermLabel() override {} /** * @param term is the term matched * @param entryId is a whole entry * @param capFirst whether the first letter should be capitalized */ void setText(const QString& term, const QByteArray& entryId, bool capFirst); void mousePressEvent(QMouseEvent* /* event*/) override; public slots: void insert(); // bool event(QEvent *event); signals: void insertTerm(const QString&); private: QByteArray m_entryId; bool m_capFirst; QAction* m_action; //used only for shortcut purposes }; } #endif diff --git a/src/common/winhelpers.cpp b/src/common/winhelpers.cpp index 5229222..a4ffcf6 100644 --- a/src/common/winhelpers.cpp +++ b/src/common/winhelpers.cpp @@ -1,36 +1,36 @@ /* **************************************************************************** This file is part of Lokalize - Copyright (C) 2018 by Simon Depiets - 2007-2017 by Nick Shaforostoff + Copyright (C) 2007-2017 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 #include #define SECURITY_WIN32 #include QString fullUserName() { ushort name[100]; unsigned long size = 99; GetUserNameW((LPWSTR)name, &size); return QString::fromUtf16(name); } diff --git a/src/completionstorage.cpp b/src/completionstorage.cpp index ef3689b..a5742ae 100644 --- a/src/completionstorage.cpp +++ b/src/completionstorage.cpp @@ -1,94 +1,95 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-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 "completionstorage.h" #include "lokalize_debug.h" #include "project.h" #include "prefs_lokalize.h" #include CompletionStorage* CompletionStorage::_instance = 0; void CompletionStorage::cleanupCompletionStorage() { delete CompletionStorage::_instance; CompletionStorage::_instance = 0; } CompletionStorage* CompletionStorage::instance() { if (_instance == 0) { _instance = new CompletionStorage(); qAddPostRoutine(CompletionStorage::cleanupCompletionStorage); } return _instance; } void CompletionStorage::scanCatalog(Catalog* catalog) { if (!catalog->numberOfEntries()) return; QTime a; a.start(); int wordCompletionLength = Settings::self()->wordCompletionLength(); /* we can't skip the scanning because there might be explicit completion triggered if (wordCompletionLength<3 || !catalog->numberOfEntries()) return; */ wordCompletionLength += 3; //only long words QString accel = Project::instance()->accel(); DocPosition pos(0); do { QString string = catalog->targetWithTags(pos).string; string.remove(accel); const QStringList& words = string.toLower().split(rxSplit, QString::SkipEmptyParts); foreach (const QString& word, words) { if (word.length() < wordCompletionLength) continue; m_words[word]++; } } while (switchNext(catalog, pos)); qCWarning(LOKALIZE_LOG) << "indexed" << catalog->url() << "for word completion in" << a.elapsed() << "msecs"; } QStringList CompletionStorage::makeCompletion(const QString& word) const { //QTime a;a.start(); if (word.isEmpty()) return QStringList(); QMultiMap hits; //we use the fact that qmap sorts it's items by keys QString cleanWord = word.toLower(); QMap::const_iterator it = m_words.lowerBound(cleanWord); while (it != m_words.constEnd() && it.key().startsWith(cleanWord)) { hits.insert(-it.value(), it.key().mid(word.length())); ++it; } //qCDebug(LOKALIZE_LOG)<<"hits generated in"< + 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 COMPLETIONSTORAGE_H #define COMPLETIONSTORAGE_H #include #include "catalog.h" class CompletionStorage { private: CompletionStorage(): rxSplit("\\W+|\\d+") {} ~CompletionStorage() {} static CompletionStorage* _instance; static void cleanupCompletionStorage(); public: static CompletionStorage* instance(); void scanCatalog(Catalog*); QStringList makeCompletion(const QString&) const; public: QRegExp rxSplit; private: QMap m_words; //how many occurencies a word has //QSet save which files we scanned? }; #endif // COMPLETIONSTORAGE_H diff --git a/src/editortab.cpp b/src/editortab.cpp index 1f1c738..e61f898 100644 --- a/src/editortab.cpp +++ b/src/editortab.cpp @@ -1,1774 +1,1775 @@ /* **************************************************************************** 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 "editortab.h" #include "xlifftextedit.h" #include "lokalize_debug.h" #include "actionproxy.h" #include "editorview.h" #include "catalog.h" #include "pos.h" #include "cmd.h" #include "completionstorage.h" #define WEBQUERY_ENABLE //views #include "msgctxtview.h" #include "alttransview.h" #include "mergeview.h" #include "cataloglistview.h" #include "glossaryview.h" #ifdef WEBQUERY_ENABLE #include "webqueryview.h" #endif #include "tmview.h" #include "binunitsview.h" #include "phaseswindow.h" #include "projectlocal.h" #include "project.h" #include "prefs.h" #include "prefs_lokalize.h" #include "languagelistmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include EditorTab::EditorTab(QWidget* parent, bool valid) : LokalizeSubwindowBase2(parent) , m_project(Project::instance()) , m_catalog(new Catalog(this)) , m_view(new EditorView(this, m_catalog/*,new keyEventHandler(this,m_catalog)*/)) , m_pologyProcessInProgress(false) , m_sonnetDialog(0) , m_spellcheckStartUndoIndex(0) , m_spellcheckStop(false) , m_currentIsApproved(true) , m_currentIsUntr(true) , m_fullPathShown(false) , m_doReplaceCalled(false) , m_find(0) , m_replace(0) , m_syncView(0) , m_syncViewSecondary(0) , m_valid(valid) , m_dbusId(-1) { //QTime chrono;chrono.start(); setAcceptDrops(true); setCentralWidget(m_view); setupStatusBar(); //--NOT called from initLater() ! setupActions(); dbusObjectPath(); connect(m_view, &EditorView::signalChanged, this, &EditorTab::msgStrChanged); msgStrChanged(); connect(SettingsController::instance(), &SettingsController::generalSettingsChanged, m_view, &EditorView::settingsChanged); connect(m_view->tabBar(), &QTabBar::currentChanged, this, &EditorTab::switchForm); connect(m_view, QOverload::of(&EditorView::gotoEntryRequested), this, QOverload::of(&EditorTab::gotoEntry)); connect(m_view, &EditorView::tmLookupRequested, this, &EditorTab::lookupSelectionInTranslationMemory); connect(this, &EditorTab::fileOpened, this, &EditorTab::indexWordsForCompletion, Qt::QueuedConnection); connect(m_catalog, &Catalog::signalFileAutoSaveFailed, this, &EditorTab::fileAutoSaveFailedWarning); //defer some work to make window appear earlier (~200 msec on my Core Duo) //QTimer::singleShot(0,this,SLOT(initLater())); //qCWarning(LOKALIZE_LOG)<isEmpty()) { emit fileAboutToBeClosed(); emit fileClosed(); emit fileClosed(currentFile()); } ids.removeAll(m_dbusId); } void EditorTab::setupStatusBar() { statusBarItems.insert(ID_STATUS_CURRENT, i18nc("@info:status message entry", "Current: %1", 0)); statusBarItems.insert(ID_STATUS_TOTAL, i18nc("@info:status message entries", "Total: %1", 0)); statusBarItems.insert(ID_STATUS_FUZZY, i18nc("@info:status message entries\n'fuzzy' in gettext terminology", "Not ready: %1", 0)); statusBarItems.insert(ID_STATUS_UNTRANS, i18nc("@info:status message entries", "Untranslated: %1", 0)); statusBarItems.insert(ID_STATUS_ISFUZZY, QString()); connect(m_catalog, &Catalog::signalNumberOfFuzziesChanged, this, &EditorTab::numberOfFuzziesChanged); connect(m_catalog, &Catalog::signalNumberOfEmptyChanged, this, &EditorTab::numberOfUntranslatedChanged); } void LokalizeSubwindowBase::reflectNonApprovedCount(int count, int total) { QString text = i18nc("@info:status message entries\n'fuzzy' in gettext terminology", "Not ready: %1", count); if (count && total) text += i18nc("percentages in statusbar", " (%1%)", int(100.0 * count / total)); statusBarItems.insert(ID_STATUS_FUZZY, text); } void LokalizeSubwindowBase::reflectUntranslatedCount(int count, int total) { QString text = i18nc("@info:status message entries", "Untranslated: %1", count); if (count && total) text += i18nc("percentages in statusbar", " (%1%)", int(100.0 * count / total)); statusBarItems.insert(ID_STATUS_UNTRANS, text); } void EditorTab::numberOfFuzziesChanged() { reflectNonApprovedCount(m_catalog->numberOfNonApproved(), m_catalog->numberOfEntries()); } void EditorTab::numberOfUntranslatedChanged() { reflectUntranslatedCount(m_catalog->numberOfUntranslated(), m_catalog->numberOfEntries()); } void EditorTab::setupActions() { //all operations that can be done after initial setup //(via QTimer::singleShot) go to initLater() setXMLFile(QStringLiteral("editorui.rc")); setUpdatedXMLFile(); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* actionCategory; KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), ac); KActionCategory* nav = new KActionCategory(i18nc("@title actions category", "Navigation"), ac); KActionCategory* edit = new KActionCategory(i18nc("@title actions category", "Editing"), ac); KActionCategory* sync1 = new KActionCategory(i18n("Synchronization 1"), ac); KActionCategory* sync2 = new KActionCategory(i18n("Synchronization 2"), ac); KActionCategory* tm = new KActionCategory(i18n("Translation Memory"), ac); KActionCategory* glossary = new KActionCategory(i18nc("@title actions category", "Glossary"), ac); //KActionCategory* tools=new KActionCategory(i18nc("@title actions category","Tools"), ac); #ifndef Q_OS_DARWIN QLocale::Language systemLang = QLocale::system().language(); #endif //BEGIN dockwidgets int i = 0; QVector altactions(ALTTRANS_SHORTCUTS); Qt::Key altlist[ALTTRANS_SHORTCUTS] = { Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5, Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9 }; QAction* altaction; for (i = 0; i < ALTTRANS_SHORTCUTS; ++i) { altaction = tm->addAction(QStringLiteral("alttrans_insert_%1").arg(i)); ac->setDefaultShortcut(altaction, QKeySequence(Qt::ALT + altlist[i])); altaction->setText(i18nc("@action:inmenu", "Insert alternate translation #%1", QString::number(i))); altactions[i] = altaction; } m_altTransView = new AltTransView(this, m_catalog, altactions); addDockWidget(Qt::BottomDockWidgetArea, m_altTransView); ac->addAction(QStringLiteral("showmsgiddiff_action"), m_altTransView->toggleViewAction()); connect(this, QOverload::of(&EditorTab::signalNewEntryDisplayed), m_altTransView, QOverload::of(&AltTransView::slotNewEntryDisplayed)); connect(m_altTransView, &AltTransView::textInsertRequested, m_view, &EditorView::insertTerm); connect(m_altTransView, &AltTransView::refreshRequested, m_view, QOverload<>::of(&EditorView::gotoEntry), Qt::QueuedConnection); connect(m_catalog, QOverload<>::of(&Catalog::signalFileLoaded), m_altTransView, &AltTransView::fileLoaded); m_syncView = new MergeView(this, m_catalog, true); addDockWidget(Qt::BottomDockWidgetArea, m_syncView); sync1->addAction(QStringLiteral("showmergeview_action"), m_syncView->toggleViewAction()); connect(this, QOverload::of(&EditorTab::signalNewEntryDisplayed), m_syncView, QOverload::of(&MergeView::slotNewEntryDisplayed)); connect(m_catalog, QOverload<>::of(&Catalog::signalFileLoaded), m_syncView, &MergeView::cleanup); connect(m_syncView, &MergeView::gotoEntry, this, QOverload::of(&EditorTab::gotoEntry)); m_syncViewSecondary = new MergeView(this, m_catalog, false); addDockWidget(Qt::BottomDockWidgetArea, m_syncViewSecondary); sync2->addAction(QStringLiteral("showmergeviewsecondary_action"), m_syncViewSecondary->toggleViewAction()); connect(this, QOverload::of(&EditorTab::signalNewEntryDisplayed), m_syncViewSecondary, QOverload::of(&MergeView::slotNewEntryDisplayed)); connect(m_catalog, QOverload<>::of(&Catalog::signalFileLoaded), m_syncViewSecondary, &MergeView::cleanup); connect(m_catalog, QOverload::of(&Catalog::signalFileLoaded), m_syncViewSecondary, QOverload::of(&MergeView::mergeOpen), Qt::QueuedConnection); connect(m_syncViewSecondary, &MergeView::gotoEntry, this, QOverload::of(&EditorTab::gotoEntry)); m_transUnitsView = new CatalogView(this, m_catalog); addDockWidget(Qt::LeftDockWidgetArea, m_transUnitsView); ac->addAction(QStringLiteral("showcatalogtreeview_action"), m_transUnitsView->toggleViewAction()); connect(this, QOverload::of(&EditorTab::signalNewEntryDisplayed), m_transUnitsView, QOverload::of(&CatalogView::slotNewEntryDisplayed)); connect(m_transUnitsView, &CatalogView::gotoEntry, this, QOverload::of(&EditorTab::gotoEntry)); connect(m_transUnitsView, &CatalogView::escaped, this, &EditorTab::setProperFocus); connect(m_syncView, &MergeView::mergeCatalogPointerChanged, m_transUnitsView, &CatalogView::setMergeCatalogPointer); m_notesView = new MsgCtxtView(this, m_catalog); addDockWidget(Qt::LeftDockWidgetArea, m_notesView); ac->addAction(QStringLiteral("showmsgctxt_action"), m_notesView->toggleViewAction()); connect(m_catalog, QOverload<>::of(&Catalog::signalFileLoaded), m_notesView, &MsgCtxtView::cleanup); connect(m_notesView, &MsgCtxtView::srcFileOpenRequested, this, &EditorTab::dispatchSrcFileOpenRequest); connect(m_view, &EditorView::signalChanged, m_notesView, &MsgCtxtView::removeErrorNotes); connect(m_notesView, &MsgCtxtView::escaped, this, &EditorTab::setProperFocus); action = edit->addAction(QStringLiteral("edit_addnote"), m_notesView, SLOT(addNoteUI())); //action->setShortcut(Qt::CTRL+glist[i]); action->setText(i18nc("@action:inmenu", "Add a note")); QVector tmactions_insert(TM_SHORTCUTS); QVector tmactions_remove(TM_SHORTCUTS); Qt::Key tmlist[TM_SHORTCUTS] = { Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5, Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9, Qt::Key_0 }; QAction* tmaction; for (i = 0; i < TM_SHORTCUTS; ++i) { // action->setVisible(false); tmaction = tm->addAction(QStringLiteral("tmquery_insert_%1").arg(i)); ac->setDefaultShortcut(tmaction, QKeySequence(Qt::CTRL + tmlist[i])); tmaction->setText(i18nc("@action:inmenu", "Insert TM suggestion #%1", i + 1)); tmactions_insert[i] = tmaction; tmaction = tm->addAction(QStringLiteral("tmquery_remove_%1").arg(i)); ac->setDefaultShortcut(tmaction, QKeySequence(Qt::CTRL + Qt::ALT + tmlist[i])); tmaction->setText(i18nc("@action:inmenu", "Remove TM suggestion #%1", i + 1)); tmactions_remove[i] = tmaction; } #ifndef Q_OS_DARWIN if (systemLang == QLocale::Czech) { ac->setDefaultShortcuts(tmactions_insert[0], QList() << QKeySequence(Qt::CTRL + tmlist[0]) << QKeySequence(Qt::CTRL + Qt::Key_Plus)); ac->setDefaultShortcuts(tmactions_remove[0], QList() << QKeySequence(Qt::CTRL + Qt::ALT + tmlist[0]) << QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Plus)); } #endif TM::TMView* _tmView = new TM::TMView(this, m_catalog, tmactions_insert, tmactions_remove); addDockWidget(Qt::BottomDockWidgetArea, _tmView); tm->addAction(QStringLiteral("showtmqueryview_action"), _tmView->toggleViewAction()); connect(_tmView, &TM::TMView::refreshRequested, m_view, QOverload<>::of(&EditorView::gotoEntry), Qt::QueuedConnection); connect(_tmView, &TM::TMView::refreshRequested, this, &EditorTab::msgStrChanged, Qt::QueuedConnection); connect(_tmView, &TM::TMView::textInsertRequested, m_view, &EditorView::insertTerm); connect(_tmView, &TM::TMView::fileOpenRequested, this, &EditorTab::fileOpenRequested); connect(this, &EditorTab::fileAboutToBeClosed, m_catalog, &Catalog::flushUpdateDBBuffer); connect(this, &EditorTab::signalNewEntryDisplayed, m_catalog, &Catalog::flushUpdateDBBuffer); connect(this, &EditorTab::signalNewEntryDisplayed, _tmView, QOverload::of(&TM::TMView::slotNewEntryDisplayed)); //do this after flushUpdateDBBuffer QVector gactions(GLOSSARY_SHORTCUTS); Qt::Key glist[GLOSSARY_SHORTCUTS] = { Qt::Key_E, Qt::Key_H, // Qt::Key_G, // Qt::Key_I, // Qt::Key_J, // Qt::Key_K, Qt::Key_K, Qt::Key_P, Qt::Key_N, // Qt::Key_Q, // Qt::Key_R, // Qt::Key_U, // Qt::Key_V, // Qt::Key_W, // Qt::Key_X, Qt::Key_Y, // Qt::Key_Z, Qt::Key_BraceLeft, Qt::Key_BraceRight, Qt::Key_Semicolon, Qt::Key_Apostrophe }; QAction* gaction; // int i=0; for (i = 0; i < GLOSSARY_SHORTCUTS; ++i) { // action->setVisible(false); gaction = glossary->addAction(QStringLiteral("glossary_insert_%1").arg(i)); ac->setDefaultShortcut(gaction, QKeySequence(Qt::CTRL + glist[i])); gaction->setText(i18nc("@action:inmenu", "Insert term translation #%1", QString::number(i))); gactions[i] = gaction; } GlossaryNS::GlossaryView* _glossaryView = new GlossaryNS::GlossaryView(this, m_catalog, gactions); addDockWidget(Qt::BottomDockWidgetArea, _glossaryView); glossary->addAction(QStringLiteral("showglossaryview_action"), _glossaryView->toggleViewAction()); connect(this, &EditorTab::signalNewEntryDisplayed, _glossaryView, QOverload::of(&GlossaryNS::GlossaryView::slotNewEntryDisplayed)); connect(_glossaryView, &GlossaryNS::GlossaryView::termInsertRequested, m_view, &EditorView::insertTerm); gaction = glossary->addAction(QStringLiteral("glossary_define"), this, SLOT(defineNewTerm())); gaction->setText(i18nc("@action:inmenu", "Define new term")); _glossaryView->addAction(gaction); _glossaryView->setContextMenuPolicy(Qt::ActionsContextMenu); BinUnitsView* binUnitsView = new BinUnitsView(m_catalog, this); addDockWidget(Qt::BottomDockWidgetArea, binUnitsView); edit->addAction(QStringLiteral("showbinunitsview_action"), binUnitsView->toggleViewAction()); connect(m_view, &EditorView::binaryUnitSelectRequested, binUnitsView, &BinUnitsView::selectUnit); //#ifdef WEBQUERY_ENABLE #if 0 QVector wqactions(WEBQUERY_SHORTCUTS); Qt::Key wqlist[WEBQUERY_SHORTCUTS] = { Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5, Qt::Key_6, Qt::Key_7, Qt::Key_8, Qt::Key_9, Qt::Key_0, }; QAction* wqaction; for (i = 0; i < WEBQUERY_SHORTCUTS; ++i) { // action->setVisible(false); wqaction = ac->addAction(QString("webquery_insert_%1").arg(i)); wqaction->setShortcut(Qt::CTRL + Qt::ALT + wqlist[i]); //wqaction->setShortcut(Qt::META+wqlist[i]); wqaction->setText(i18nc("@action:inmenu", "Insert WebQuery result #%1", i)); wqactions[i] = wqaction; } WebQueryView* _webQueryView = new WebQueryView(this, m_catalog, wqactions); addDockWidget(Qt::BottomDockWidgetArea, _webQueryView); ac->addAction(QStringLiteral("showwebqueryview_action"), _webQueryView->toggleViewAction()); connect(this, &EditorTab::signalNewEntryDisplayed, _webQueryView, SLOT(slotNewEntryDisplayed(DocPosition))); connect(_webQueryView, SIGNAL(textInsertRequested(QString)), m_view, SLOT(insertTerm(QString))); #endif //END dockwidgets actionCategory = file; // File action = file->addAction(KStandardAction::Save, this, SLOT(saveFile())); // action->setEnabled(false); // connect (m_catalog,SIGNAL(cleanChanged(bool)),action,SLOT(setDisabled(bool))); connect(m_catalog, &Catalog::cleanChanged, this, &EditorTab::setModificationSign); file->addAction(KStandardAction::SaveAs, this, SLOT(saveFileAs())); //action = KStandardAction::quit(qApp, SLOT(quit()), ac); //action->setText(i18nc("@action:inmenu","Close all Lokalize windows")); //KStandardAction::quit(kapp, SLOT(quit()), ac); //KStandardAction::quit(this, SLOT(deleteLater()), ac); #define ADD_ACTION_SHORTCUT_ICON(_name,_text,_shortcut,_icon)\ action = actionCategory->addAction(QStringLiteral(_name));\ action->setText(_text);\ action->setIcon(QIcon::fromTheme(QStringLiteral(_icon)));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut )); #define ADD_ACTION_SHORTCUT(_name,_text,_shortcut)\ action = actionCategory->addAction(QStringLiteral(_name));\ action->setText(_text);\ ac->setDefaultShortcut(action, QKeySequence( _shortcut )); action = actionCategory->addAction(QStringLiteral("file_phases")); action->setText(i18nc("@action:inmenu", "Phases...")); connect(action, &QAction::triggered, this, &EditorTab::openPhasesWindow); ADD_ACTION_SHORTCUT("file_wordcount", i18nc("@action:inmenu", "Word count"), Qt::CTRL + Qt::ALT + Qt::Key_C) connect(action, &QAction::triggered, this, &EditorTab::displayWordCount); ADD_ACTION_SHORTCUT("file_cleartarget", i18nc("@action:inmenu", "Clear all translated entries"), Qt::CTRL + Qt::ALT + Qt::Key_D) connect(action, &QAction::triggered, this, &EditorTab::clearTranslatedEntries); ADD_ACTION_SHORTCUT("file_pology", i18nc("@action:inmenu", "Launch the Pology command on this file"), Qt::CTRL + Qt::ALT + Qt::Key_P) action->setEnabled(Settings::self()->pologyEnabled()); connect(action, &QAction::triggered, this, &EditorTab::launchPology); ADD_ACTION_SHORTCUT("file_xliff2odf", i18nc("@action:inmenu", "Merge translation into OpenDocument"), Qt::CTRL + Qt::Key_Backslash) connect(action, &QAction::triggered, this, &EditorTab::mergeIntoOpenDocument); connect(this, &EditorTab::xliffFileOpened, action, &QAction::setVisible); action->setVisible(false); //Edit actionCategory = edit; action = edit->addAction(KStandardAction::Undo, this, SLOT(undo())); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::undoRequested, this, &EditorTab::undo); connect(m_catalog, &Catalog::canUndoChanged, action, &QAction::setEnabled); action->setEnabled(false); action = edit->addAction(KStandardAction::Redo, this, SLOT(redo())); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::redoRequested, this, &EditorTab::redo); connect(m_catalog, &Catalog::canRedoChanged, action, &QAction::setEnabled); action->setEnabled(false); action = nav->addAction(KStandardAction::Find, this, SLOT(find())); action = nav->addAction(KStandardAction::FindNext, this, SLOT(findNext())); action = nav->addAction(KStandardAction::FindPrev, this, SLOT(findPrev())); action->setText(i18nc("@action:inmenu", "Change searching direction")); action = edit->addAction(KStandardAction::Replace, this, SLOT(replace())); connect(m_view, &EditorView::findRequested, this, &EditorTab::find); connect(m_view, &EditorView::findNextRequested, this, QOverload<>::of(&EditorTab::findNext)); connect(m_view, &EditorView::replaceRequested, this, &EditorTab::replace); action = actionCategory->addAction(QStringLiteral("edit_approve"), new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("approved")), i18nc("@option:check whether message is marked as translated/reviewed/approved (depending on your role)", "Approved"), this)); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_U)); action->setCheckable(true); connect(action, &QAction::triggered, m_view, &EditorView::toggleApprovement); connect(m_view, &EditorView::signalApprovedEntryDisplayed, this, &EditorTab::signalApprovedEntryDisplayed); connect(this, &EditorTab::signalApprovedEntryDisplayed, action, &QAction::setChecked); connect(this, &EditorTab::signalApprovedEntryDisplayed, this, &EditorTab::msgStrChanged, Qt::QueuedConnection); m_approveAction = action; m_stateAction = action; connect(Project::local(), &ProjectLocal::configChanged, this, &EditorTab::setApproveActionTitle); connect(m_catalog, &Catalog::activePhaseChanged, this, &EditorTab::setApproveActionTitle); connect(m_stateAction->menu(), &QMenu::aboutToShow, this, &EditorTab::showStatesMenu); connect(m_stateAction->menu(), &QMenu::triggered, this, &EditorTab::setState); action = actionCategory->addAction(QStringLiteral("edit_approve_go_fuzzyUntr")); action->setText(i18nc("@action:inmenu", "Approve and go to next")); connect(action, &QAction::triggered, this, &EditorTab::toggleApprovementGotoNextFuzzyUntr); m_approveAndGoAction = action; setApproveActionTitle(); action = actionCategory->addAction(QStringLiteral("edit_nonequiv"), m_view, SLOT(setEquivTrans(bool))); action->setText(i18nc("@action:inmenu", "Equivalent translation")); action->setCheckable(true); connect(this, &EditorTab::signalEquivTranslatedEntryDisplayed, action, &QAction::setChecked); #ifndef Q_OS_DARWIN int copyShortcut = Qt::CTRL + Qt::Key_Space; if (Q_UNLIKELY(systemLang == QLocale::Korean || systemLang == QLocale::Japanese || systemLang == QLocale::Chinese )) copyShortcut = Qt::ALT + Qt::Key_Space; #else int copyShortcut = Qt::META + Qt::Key_Space; #endif ADD_ACTION_SHORTCUT_ICON("edit_msgid2msgstr", i18nc("@action:inmenu", "Copy source to target"), copyShortcut, "msgid2msgstr") connect(action, &QAction::triggered, (const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::source2target); ADD_ACTION_SHORTCUT("edit_unwrap-target", i18nc("@action:inmenu", "Unwrap target"), Qt::CTRL + Qt::Key_I) connect(action, &QAction::triggered, m_view, QOverload<>::of(&EditorView::unwrap)); action = edit->addAction(QStringLiteral("edit_clear-target"), m_view->viewPort(), SLOT(removeTargetSubstring())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D)); action->setText(i18nc("@action:inmenu", "Clear")); action = edit->addAction(QStringLiteral("edit_tagmenu"), m_view->viewPort(), SLOT(tagMenu())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); action->setText(i18nc("@action:inmenu", "Insert Tag")); action = edit->addAction(QStringLiteral("edit_tagimmediate"), m_view->viewPort(), SLOT(tagImmediate())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_M)); action->setText(i18nc("@action:inmenu", "Insert Next Tag")); action = edit->addAction(QStringLiteral("edit_completion"), m_view, SIGNAL(doExplicitCompletion())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Space)); action->setText(i18nc("@action:inmenu", "Completion")); action = edit->addAction(QStringLiteral("edit_spellreplace"), m_view->viewPort(), SLOT(spellReplace())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Equal)); action->setText(i18nc("@action:inmenu", "Replace with best spellcheck suggestion")); // action = ac->addAction("glossary_define",m_view,SLOT(defineNewTerm())); // action->setText(i18nc("@action:inmenu","Define new term")); // Go actionCategory = nav; action = nav->addAction(KStandardAction::Next, this, SLOT(gotoNext())); action->setText(i18nc("@action:inmenu entry", "&Next")); connect(this, &EditorTab::signalLastDisplayed, action, &QAction::setDisabled); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoNextRequested, this, &EditorTab::gotoNext); action = nav->addAction(KStandardAction::Prior, this, SLOT(gotoPrev())); action->setText(i18nc("@action:inmenu entry", "&Previous")); connect(this, &EditorTab::signalFirstDisplayed, action, &QAction::setDisabled); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoPrevRequested, this, &EditorTab::gotoPrev); action = nav->addAction(KStandardAction::FirstPage, this, SLOT(gotoFirst())); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoFirstRequested, this, &EditorTab::gotoFirst); action->setText(i18nc("@action:inmenu", "&First Entry")); action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Home)); connect(this, &EditorTab::signalFirstDisplayed, action, &QAction::setDisabled); action = nav->addAction(KStandardAction::LastPage, this, SLOT(gotoLast())); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoLastRequested, this, &EditorTab::gotoLast); action->setText(i18nc("@action:inmenu", "&Last Entry")); action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_End)); connect(this, &EditorTab::signalLastDisplayed, action, &QAction::setDisabled); action = nav->addAction(KStandardAction::GotoPage, this, SLOT(gotoEntry())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_G)); action->setText(i18nc("@action:inmenu", "Entry by number")); 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, &EditorTab::gotoPrevFuzzy); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoPrevFuzzyRequested, this, &EditorTab::gotoPrevFuzzy); connect(this, &EditorTab::signalPriorFuzzyAvailable, action, &QAction::setEnabled); 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, &EditorTab::gotoNextFuzzy); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoNextFuzzyRequested, this, &EditorTab::gotoNextFuzzy); connect(this, &EditorTab::signalNextFuzzyAvailable, action, &QAction::setEnabled); ADD_ACTION_SHORTCUT_ICON("go_prev_untrans", i18nc("@action:inmenu", "Previous untranslated"), Qt::ALT + Qt::Key_PageUp, "prevuntranslated") connect(action, &QAction::triggered, this, &EditorTab::gotoPrevUntranslated); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoPrevUntranslatedRequested, this, &EditorTab::gotoPrevUntranslated); connect(this, &EditorTab::signalPriorUntranslatedAvailable, action, &QAction::setEnabled); ADD_ACTION_SHORTCUT_ICON("go_next_untrans", i18nc("@action:inmenu", "Next untranslated"), Qt::ALT + Qt::Key_PageDown, "nextuntranslated") connect(action, &QAction::triggered, this, &EditorTab::gotoNextUntranslated); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoNextUntranslatedRequested, this, &EditorTab::gotoNextUntranslated); connect(this, &EditorTab::signalNextUntranslatedAvailable, action, &QAction::setEnabled); 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/*ALT*/ + Qt::Key_PageUp, "prevfuzzyuntrans") connect(action, &QAction::triggered, this, &EditorTab::gotoPrevFuzzyUntr); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoPrevFuzzyUntrRequested, this, &EditorTab::gotoPrevFuzzyUntr); connect(this, &EditorTab::signalPriorFuzzyOrUntrAvailable, action, &QAction::setEnabled); 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, QOverload<>::of(&EditorTab::gotoNextFuzzyUntr)); connect((const TranslationUnitTextEdit*)m_view->viewPort(), &TranslationUnitTextEdit::gotoNextFuzzyUntrRequested, this, QOverload<>::of(&EditorTab::gotoNextFuzzyUntr)); connect(this, &EditorTab::signalNextFuzzyOrUntrAvailable, action, &QAction::setEnabled); action = nav->addAction(QStringLiteral("go_focus_earch_line"), m_transUnitsView, SLOT(setFocus())); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_L)); action->setText(i18nc("@action:inmenu", "Focus the search line of Translation Units view")); //Bookmarks action = nav->addAction(KStandardAction::AddBookmark, m_view, SLOT(toggleBookmark(bool))); //action = ac->addAction("bookmark_do"); action->setText(i18nc("@option:check", "Bookmark message")); action->setCheckable(true); //connect(action, SIGNAL(triggered(bool)),m_view,SLOT(toggleBookmark(bool))); connect(this, &EditorTab::signalBookmarkDisplayed, action, &QAction::setChecked); action = nav->addAction(QStringLiteral("bookmark_prior"), this, SLOT(gotoPrevBookmark())); action->setText(i18nc("@action:inmenu", "Previous bookmark")); connect(this, &EditorTab::signalPriorBookmarkAvailable, action, &QAction::setEnabled); action = nav->addAction(QStringLiteral("bookmark_next"), this, SLOT(gotoNextBookmark())); action->setText(i18nc("@action:inmenu", "Next bookmark")); connect(this, &EditorTab::signalNextBookmarkAvailable, action, &QAction::setEnabled); //Tools edit->addAction(KStandardAction::Spelling, this, SLOT(spellcheck())); actionCategory = tm; // xgettext: no-c-format ADD_ACTION_SHORTCUT("tools_tm_batch", i18nc("@action:inmenu", "Fill in all exact suggestions"), Qt::CTRL + Qt::ALT + Qt::Key_B) connect(action, &QAction::triggered, _tmView, &TM::TMView::slotBatchTranslate); // xgettext: no-c-format ADD_ACTION_SHORTCUT("tools_tm_batch_fuzzy", i18nc("@action:inmenu", "Fill in all exact suggestions and mark as fuzzy"), Qt::CTRL + Qt::ALT + Qt::Key_N) connect(action, &QAction::triggered, _tmView, &TM::TMView::slotBatchTranslateFuzzy); //MergeMode action = sync1->addAction(QStringLiteral("merge_open"), m_syncView, SLOT(mergeOpen())); action->setText(i18nc("@action:inmenu", "Open file for sync/merge")); action->setStatusTip(i18nc("@info:status", "Open catalog to be merged into the current one / replicate base file changes to")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); m_syncView->addAction(action); action = sync1->addAction(QStringLiteral("merge_prev"), m_syncView, SLOT(gotoPrevChanged())); action->setText(i18nc("@action:inmenu", "Previous different")); action->setStatusTip(i18nc("@info:status", "Previous entry which is translated differently in the file being merged, including empty translations in merge source")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Up)); connect(m_syncView, &MergeView::signalPriorChangedAvailable, action, &QAction::setEnabled); m_syncView->addAction(action); action = sync1->addAction(QStringLiteral("merge_next"), m_syncView, SLOT(gotoNextChanged())); action->setText(i18nc("@action:inmenu", "Next different")); action->setStatusTip(i18nc("@info:status", "Next entry which is translated differently in the file being merged, including empty translations in merge source")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Down)); connect(m_syncView, &MergeView::signalNextChangedAvailable, action, &QAction::setEnabled); m_syncView->addAction(action); action = sync1->addAction(QStringLiteral("merge_nextapproved"), m_syncView, SLOT(gotoNextChangedApproved())); action->setText(i18nc("@action:inmenu", "Next different approved")); ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::META + Qt::Key_Down)); connect(m_syncView, &MergeView::signalNextChangedAvailable, action, &QAction::setEnabled); m_syncView->addAction(action); action = sync1->addAction(QStringLiteral("merge_accept"), m_syncView, SLOT(mergeAccept())); action->setText(i18nc("@action:inmenu", "Copy from merging source")); action->setEnabled(false); ac->setDefaultShortcut(action, QKeySequence(Qt::ALT + Qt::Key_Return)); connect(m_syncView, &MergeView::signalEntryWithMergeDisplayed, action, &QAction::setEnabled); m_syncView->addAction(action); action = sync1->addAction(QStringLiteral("merge_acceptnew"), m_syncView, SLOT(mergeAcceptAllForEmpty())); action->setText(i18nc("@action:inmenu", "Copy all new translations")); action->setStatusTip(i18nc("@info:status", "This changes only empty and non-ready entries in base file")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_A)); connect(m_syncView, &MergeView::mergeCatalogAvailable, action, &QAction::setEnabled); m_syncView->addAction(action); //action->setShortcut(Qt::ALT+Qt::Key_E); action = sync1->addAction(QStringLiteral("merge_back"), m_syncView, SLOT(mergeBack())); action->setText(i18nc("@action:inmenu", "Copy to merging source")); connect(m_syncView, &MergeView::mergeCatalogAvailable, action, &QAction::setEnabled); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Return)); m_syncView->addAction(action); //Secondary merge action = sync2->addAction(QStringLiteral("mergesecondary_open"), m_syncViewSecondary, SLOT(mergeOpen())); action->setText(i18nc("@action:inmenu", "Open file for secondary sync")); action->setStatusTip(i18nc("@info:status", "Open catalog to be merged into the current one / replicate base file changes to")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); m_syncViewSecondary->addAction(action); action = sync2->addAction(QStringLiteral("mergesecondary_prev"), m_syncViewSecondary, SLOT(gotoPrevChanged())); action->setText(i18nc("@action:inmenu", "Previous different")); action->setStatusTip(i18nc("@info:status", "Previous entry which is translated differently in the file being merged, including empty translations in merge source")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); connect(m_syncView, &MergeView::signalPriorChangedAvailable, action, &QAction::setEnabled); m_syncViewSecondary->addAction(action); action = sync2->addAction(QStringLiteral("mergesecondary_next"), m_syncViewSecondary, SLOT(gotoNextChanged())); action->setText(i18nc("@action:inmenu", "Next different")); action->setStatusTip(i18nc("@info:status", "Next entry which is translated differently in the file being merged, including empty translations in merge source")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); connect(m_syncView, &MergeView::signalNextChangedAvailable, action, &QAction::setEnabled); m_syncViewSecondary->addAction(action); action = sync2->addAction(QStringLiteral("mergesecondary_accept"), m_syncViewSecondary, SLOT(mergeAccept())); action->setText(i18nc("@action:inmenu", "Copy from merging source")); connect(m_syncView, &MergeView::signalEntryWithMergeDisplayed, action, &QAction::setEnabled); m_syncViewSecondary->addAction(action); action = sync2->addAction(QStringLiteral("mergesecondary_acceptnew"), m_syncViewSecondary, SLOT(mergeAcceptAllForEmpty())); action->setText(i18nc("@action:inmenu", "Copy all new translations")); action->setStatusTip(i18nc("@info:status", "This changes only empty entries")); action->setToolTip(action->statusTip()); action->setWhatsThis(action->statusTip()); m_syncViewSecondary->addAction(action); action = sync2->addAction(QStringLiteral("mergesecondary_back"), m_syncViewSecondary, SLOT(mergeBack())); action->setText(i18nc("@action:inmenu", "Copy to merging source")); m_syncViewSecondary->addAction(action); } void EditorTab::setProperFocus() { m_view->setProperFocus(); } void EditorTab::hideDocks() { if (m_transUnitsView->isFloating()) m_transUnitsView->hide(); } void EditorTab::showDocks() { return; if (m_transUnitsView->isFloating()) m_transUnitsView->show(); } void EditorTab::setProperCaption(QString title, bool modified) { if (m_catalog->autoSaveRecovered()) title += ' ' + i18nc("editor tab name", "(recovered)"); setWindowTitle(title + QStringLiteral(" [*]")); setWindowModified(modified); } void EditorTab::setFullPathShown(bool fullPathShown) { m_fullPathShown = fullPathShown; updateCaptionPath(); setModificationSign(); } void EditorTab::setModificationSign() { bool clean = m_catalog->isClean() && !m_syncView->isModified() && !m_syncViewSecondary->isModified(); setProperCaption(_captionPath, !clean); } void EditorTab::updateCaptionPath() { QString url = m_catalog->url(); if (!m_project->isLoaded()) { _captionPath = url; return; } if (!m_fullPathShown) { _captionPath = QFileInfo(url).fileName(); return; } _captionPath = QDir(QFileInfo(m_project->path()).absolutePath()).relativeFilePath(url); if (_captionPath.contains(QLatin1String("../.."))) _captionPath = url; else if (_captionPath.startsWith(QLatin1String("./"))) _captionPath = _captionPath.mid(2); } bool EditorTab::fileOpen(QString filePath, QString suggestedDirPath, bool silent) { if (!m_catalog->isClean()) { switch (KMessageBox::warningYesNoCancel(SettingsController::instance()->mainWindowPtr(), i18nc("@info", "The document contains unsaved changes.\n" "Do you want to save your changes or discard them?"), i18nc("@title:window", "Warning"), KStandardGuiItem::save(), KStandardGuiItem::discard()) ) { case KMessageBox::Yes: if (!saveFile()) return false; break; case KMessageBox::Cancel: return false; default:; } } if (suggestedDirPath.isEmpty()) suggestedDirPath = m_catalog->url(); QString saidPath; if (filePath.isEmpty()) { //Prevent crashes //Project::instance()->model()->weaver()->suspend(); //KDE5PORT use mutex if the crash is still there with kfilemetadata library filePath = QFileDialog::getOpenFileName(SettingsController::instance()->mainWindowPtr(), i18nc("@title:window", "Select translation file"), suggestedDirPath, Catalog::supportedFileTypes(true));//" text/x-gettext-translation-template"); //Project::instance()->model()->weaver()->resume(); //TODO application/x-xliff, windows: just extensions //originalPath=url.path(); never used } else if (!QFile::exists(filePath) && Project::instance()->isLoaded()) { //check if we are opening template QString newPath = filePath; newPath.replace(Project::instance()->poDir(), Project::instance()->potDir()); if (QFile::exists(newPath) || QFile::exists(newPath += 't')) { saidPath = filePath; filePath = newPath; } } if (filePath.isEmpty()) return false; QApplication::setOverrideCursor(Qt::WaitCursor); QString prevFilePath = currentFilePath(); bool wasOpen = !m_catalog->isEmpty(); if (wasOpen) emit fileAboutToBeClosed(); int errorLine = m_catalog->loadFromUrl(filePath, saidPath); if (wasOpen && errorLine == 0) { emit fileClosed(); emit fileClosed(prevFilePath); } QApplication::restoreOverrideCursor(); if (errorLine == 0) { statusBarItems.insert(ID_STATUS_TOTAL, i18nc("@info:status message entries", "Total: %1", m_catalog->numberOfEntries())); numberOfUntranslatedChanged(); numberOfFuzziesChanged(); m_currentPos.entry = -1; //so the signals are emitted DocPosition pos(0); //we delay gotoEntry(pos) until project is loaded; //Project if (!m_project->isLoaded()) { QFileInfo fileInfo(filePath); //search for it int i = 4; QDir dir = fileInfo.dir(); QStringList proj(QStringLiteral("*.lokalize")); dir.setNameFilters(proj); while (--i && !dir.isRoot() && !m_project->isLoaded()) { if (dir.entryList().isEmpty()) { if (!dir.cdUp()) break; } else m_project->load(dir.absoluteFilePath(dir.entryList().first())); } //enforce autosync m_syncViewSecondary->mergeOpen(filePath); if (!m_project->isLoaded()) { if (m_project->desirablePath().isEmpty()) m_project->setDesirablePath(fileInfo.absolutePath() + QStringLiteral("/index.lokalize")); if (m_catalog->targetLangCode().isEmpty() /*&& m_project->targetLangCode().length()*/) m_catalog->setTargetLangCode(getTargetLangCode(fileInfo.fileName())); //_project->setLangCode(m_catalog->targetLangCode()); } } if (m_catalog->targetLangCode().isEmpty() /*&& m_project->targetLangCode().length()*/) m_catalog->setTargetLangCode(Project::instance()->targetLangCode()); gotoEntry(pos); updateCaptionPath(); setModificationSign(); //OK!!! emit xliffFileOpened(m_catalog->type() == Xliff); emit fileOpened(); return true; } if (!silent) { if (errorLine > 0) KMessageBox::error(this, i18nc("@info", "Error opening the file %1, line: %2", filePath, errorLine)); else KMessageBox::error(this, i18nc("@info", "Error opening the file %1", filePath)); } return false; } bool EditorTab::saveFileAs(const QString& defaultPath) { QString filePath = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Save File As"), QFileInfo(defaultPath.isEmpty() ? m_catalog->url() : defaultPath).absoluteFilePath(), m_catalog->fileType()); if (filePath.isEmpty()) return false; if (!Catalog::extIsSupported(filePath) && m_catalog->url().contains('.')) filePath += m_catalog->url().midRef(m_catalog->url().lastIndexOf('.')); return saveFile(filePath); } bool EditorTab::saveFile(const QString& filePath) { bool clean = m_catalog->isClean() && !m_syncView->isModified() && !m_syncViewSecondary->isModified() && filePath == m_catalog->url(); if (clean) return true; if (m_catalog->isClean() && filePath.isEmpty()) { emit m_catalog->signalFileSaved(); return true; } if (m_catalog->saveToUrl(filePath)) { updateCaptionPath(); setModificationSign(); emit fileSaved(filePath); return true; } const QString errorFilePath = filePath.isEmpty() ? m_catalog->url() : filePath; if (KMessageBox::Continue == KMessageBox::warningContinueCancel(this, i18nc("@info", "Error saving the file %1\n" "Do you want to save to another file or cancel?", errorFilePath), i18nc("@title", "Error"), KStandardGuiItem::save()) ) return saveFileAs(errorFilePath); return false; } void EditorTab::fileAutoSaveFailedWarning(const QString& fileName) { KMessageBox::information(this, i18nc("@info", "Could not perform file autosaving.\n" "The target file was %1.", fileName)); } EditorState EditorTab::state() { EditorState state; state.dockWidgets = saveState(); state.filePath = m_catalog->url(); state.mergeFilePath = m_syncView->filePath(); state.entry = m_currentPos.entry; //state.offset=_currentPos.offset; return state; } bool EditorTab::queryClose() { bool clean = m_catalog->isClean() && !m_syncView->isModified() && !m_syncViewSecondary->isModified(); if (clean) return true; //TODO caption switch (KMessageBox::warningYesNoCancel(this, i18nc("@info", "The document contains unsaved changes.\n" "Do you want to save your changes or discard them?"), i18nc("@title:window", "Warning"), KStandardGuiItem::save(), KStandardGuiItem::discard())) { case KMessageBox::Yes: return saveFile(); case KMessageBox::No: return true; default: return false; } } void EditorTab::undo() { gotoEntry(m_catalog->undo(), 0); msgStrChanged(); } void EditorTab::redo() { gotoEntry(m_catalog->redo(), 0); msgStrChanged(); } void EditorTab::gotoEntry() { bool ok = false; DocPosition pos = m_currentPos; pos.entry = QInputDialog::getInt(this, i18nc("@title", "Jump to Entry"), i18nc("@label:spinbox", "Enter entry number:"), pos.entry, 1, m_catalog->numberOfEntries(), 1, &ok); if (ok) { --(pos.entry); gotoEntry(pos); } } void EditorTab::gotoEntry(DocPosition pos) { return gotoEntry(pos, 0); } void EditorTab::gotoEntry(DocPosition pos, int selection) { //specially for dbus users if (pos.entry >= m_catalog->numberOfEntries() || pos.entry < 0) return; if (!m_catalog->isPlural(pos)) pos.form = 0; m_currentPos.part = pos.part; //for searching; //UndefPart => called on fuzzy toggle bool newEntry = m_currentPos.entry != pos.entry || m_currentPos.form != pos.form; if (newEntry || pos.part == DocPosition::Comment) { m_notesView->gotoEntry(pos, pos.part == DocPosition::Comment ? selection : 0); if (pos.part == DocPosition::Comment) { pos.form = 0; pos.offset = 0; selection = 0; } } m_view->gotoEntry(pos, selection); if (pos.part == DocPosition::UndefPart) m_currentPos.part = DocPosition::Target; //QTime time; time.start(); if (newEntry) { m_currentPos = pos; if (true) { emit signalNewEntryDisplayed(pos); emit entryDisplayed(); emit signalFirstDisplayed(pos.entry == m_transUnitsView->firstEntryNumber()); emit signalLastDisplayed(pos.entry == m_transUnitsView->lastEntryNumber()); emit signalPriorFuzzyAvailable(pos.entry > m_catalog->firstFuzzyIndex()); emit signalNextFuzzyAvailable(pos.entry < m_catalog->lastFuzzyIndex()); emit signalPriorUntranslatedAvailable(pos.entry > m_catalog->firstUntranslatedIndex()); emit signalNextUntranslatedAvailable(pos.entry < m_catalog->lastUntranslatedIndex()); emit signalPriorFuzzyOrUntrAvailable(pos.entry > m_catalog->firstFuzzyIndex() || pos.entry > m_catalog->firstUntranslatedIndex() ); emit signalNextFuzzyOrUntrAvailable(pos.entry < m_catalog->lastFuzzyIndex() || pos.entry < m_catalog->lastUntranslatedIndex()); emit signalPriorBookmarkAvailable(pos.entry > m_catalog->firstBookmarkIndex()); emit signalNextBookmarkAvailable(pos.entry < m_catalog->lastBookmarkIndex()); emit signalBookmarkDisplayed(m_catalog->isBookmarked(pos.entry)); emit signalEquivTranslatedEntryDisplayed(m_catalog->isEquivTrans(pos)); emit signalApprovedEntryDisplayed(m_catalog->isApproved(pos)); } } statusBarItems.insert(ID_STATUS_CURRENT, i18nc("@info:status", "Current: %1", m_currentPos.entry + 1)); //qCDebug(LOKALIZE_LOG)<<"ELA "<msgstr(m_currentPos).isEmpty(); bool isApproved = m_catalog->isApproved(m_currentPos); if (isUntr == m_currentIsUntr && isApproved == m_currentIsApproved) return; QString msg; if (isUntr) msg = i18nc("@info:status", "Untranslated"); else if (isApproved)msg = i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"); else msg = i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"); /* else statusBar()->changeItem("",ID_STATUS_ISFUZZY);*/ statusBarItems.insert(ID_STATUS_ISFUZZY, msg); m_currentIsUntr = isUntr; m_currentIsApproved = isApproved; } void EditorTab::switchForm(int newForm) { if (m_currentPos.form == newForm) return; DocPosition pos = m_currentPos; pos.form = newForm; gotoEntry(pos); } void EditorTab::gotoNextUnfiltered() { DocPosition pos = m_currentPos; if (switchNext(m_catalog, pos)) gotoEntry(pos); } void EditorTab::gotoPrevUnfiltered() { DocPosition pos = m_currentPos; if (switchPrev(m_catalog, pos)) gotoEntry(pos); } void EditorTab::gotoFirstUnfiltered() { gotoEntry(DocPosition(0)); } void EditorTab::gotoLastUnfiltered() { gotoEntry(DocPosition(m_catalog->numberOfEntries() - 1)); } void EditorTab::gotoFirst() { DocPosition pos = DocPosition(m_transUnitsView->firstEntryNumber()); if (pos.entry != -1) gotoEntry(pos); } void EditorTab::gotoLast() { DocPosition pos = DocPosition(m_transUnitsView->lastEntryNumber()); if (pos.entry != -1) gotoEntry(pos); } void EditorTab::gotoNext() { DocPosition pos = m_currentPos; if (m_catalog->isPlural(pos) && pos.form + 1 < m_catalog->numberOfPluralForms()) pos.form++; else pos = DocPosition(m_transUnitsView->nextEntryNumber()); if (pos.entry != -1) gotoEntry(pos); } void EditorTab::gotoPrev() { DocPosition pos = m_currentPos; if (m_catalog->isPlural(pos) && pos.form > 0) pos.form--; else pos = DocPosition(m_transUnitsView->prevEntryNumber()); if (pos.entry != -1) gotoEntry(pos); } void EditorTab::gotoPrevFuzzy() { DocPosition pos; if ((pos.entry = m_catalog->prevFuzzyIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } void EditorTab::gotoNextFuzzy() { DocPosition pos; if ((pos.entry = m_catalog->nextFuzzyIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } void EditorTab::gotoPrevUntranslated() { DocPosition pos; if ((pos.entry = m_catalog->prevUntranslatedIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } void EditorTab::gotoNextUntranslated() { DocPosition pos; if ((pos.entry = m_catalog->nextUntranslatedIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } void EditorTab::gotoPrevFuzzyUntr() { DocPosition pos; short fu = m_catalog->prevFuzzyIndex(m_currentPos.entry); short un = m_catalog->prevUntranslatedIndex(m_currentPos.entry); pos.entry = fu > un ? fu : un; if (pos.entry == -1) return; gotoEntry(pos); } bool EditorTab::gotoNextFuzzyUntr() { return gotoNextFuzzyUntr(DocPosition()); } bool EditorTab::gotoNextFuzzyUntr(const DocPosition& p) { int index = (p.entry == -1) ? m_currentPos.entry : p.entry; DocPosition pos; short fu = m_catalog->nextFuzzyIndex(index); short un = m_catalog->nextUntranslatedIndex(index); if ((fu == -1) && (un == -1)) return false; if (fu == -1) fu = un; else if (un == -1) un = fu; pos.entry = fu < un ? fu : un; gotoEntry(pos); return true; } void EditorTab::toggleApprovementGotoNextFuzzyUntr() { if (!m_catalog->isApproved(m_currentPos.entry)) m_view->toggleApprovement(); if (!gotoNextFuzzyUntr()) gotoNextFuzzyUntr(DocPosition(-2));//so that we don't skip the first } void EditorTab::setApproveActionTitle() { const char* const titles[] = { I18N_NOOP2("@option:check trans-unit state", "Translated"), I18N_NOOP2("@option:check trans-unit state", "Signed-off"), I18N_NOOP2("@option:check trans-unit state", "Approved") }; const char* const helpText[] = { I18N_NOOP2("@info:tooltip", "Translation is done (although still may need a review)"), I18N_NOOP2("@info:tooltip", "Translation has received positive review"), I18N_NOOP2("@info:tooltip", "Entry is fully localized (i.e. final)") }; int role = m_catalog->activePhaseRole(); if (role == ProjectLocal::Undefined) role = Project::local()->role(); m_approveAction->setText(i18nc("@option:check trans-unit state", titles[role])); m_approveAction->setToolTip(i18nc("@info:tooltip", helpText[role])); m_approveAndGoAction->setVisible(role == ProjectLocal::Approver); } void EditorTab::showStatesMenu() { m_stateAction->menu()->clear(); if (!(m_catalog->capabilities()&ExtendedStates)) { QAction* a = m_stateAction->menu()->addAction(i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review")); a->setData(QVariant(-1)); a->setCheckable(true); a->setChecked(!m_catalog->isApproved(m_currentPos)); a = m_stateAction->menu()->addAction(i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready")); a->setData(QVariant(-2)); a->setCheckable(true); a->setChecked(m_catalog->isApproved(m_currentPos)); return; } TargetState state = m_catalog->state(m_currentPos); const char* const* states = Catalog::states(); for (int i = 0; i < StateCount; ++i) { QAction* a = m_stateAction->menu()->addAction(i18n(states[i])); a->setData(QVariant(i)); a->setCheckable(true); a->setChecked(state == i); if (i == New || i == Translated || i == Final) m_stateAction->menu()->addSeparator(); } } void EditorTab::setState(QAction* a) { if (!(m_catalog->capabilities()&ExtendedStates)) m_view->toggleApprovement(); else m_view->setState(TargetState(a->data().toInt())); m_stateAction->menu()->clear(); } void EditorTab::openPhasesWindow() { PhasesWindow w(m_catalog, this); w.exec(); } void EditorTab::gotoPrevBookmark() { DocPosition pos; if ((pos.entry = m_catalog->prevBookmarkIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } void EditorTab::gotoNextBookmark() { DocPosition pos; if ((pos.entry = m_catalog->nextBookmarkIndex(m_currentPos.entry)) == -1) return; gotoEntry(pos); } //wrapper for cmdline handling... void EditorTab::mergeOpen(QString mergeFilePath) { m_syncView->mergeOpen(mergeFilePath); } //HACK to prevent redundant repaintings when widget isn't visible void EditorTab::paintEvent(QPaintEvent* event) { if (QMdiSubWindow* sw = qobject_cast(parent())) { if (sw->mdiArea()->currentSubWindow() != sw) return; } LokalizeSubwindowBase2::paintEvent(event); } void EditorTab::indexWordsForCompletion() { CompletionStorage::instance()->scanCatalog(m_catalog); } void EditorTab::launchPology() { if (!m_pologyProcessInProgress) { QString command = Settings::self()->pologyCommandFile().replace(QStringLiteral("%f"), QStringLiteral("\"") + currentFilePath() + QStringLiteral("\"")); m_pologyProcess = new KProcess; m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); qCWarning(LOKALIZE_LOG) << "Launching pology command: " << command; connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &EditorTab::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 EditorTab::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 EditorTab::clearTranslatedEntries() { switch (KMessageBox::warningYesNoCancel(this, i18nc("@info", "This will delete all the translations from the file.\n" "Do you really want to clear all translated entries?"), i18nc("@title:window", "Warning"), KStandardGuiItem::yes(), KStandardGuiItem::no())) { case KMessageBox::Yes: { DocPosition pos(0); do { removeTargetSubstring(m_catalog, pos); } while (switchNext(m_catalog, pos)); msgStrChanged(); gotoEntry(m_currentPos); } default:; } } void EditorTab::displayWordCount() { //TODO in trans and fuzzy separately int sourceCount = 0; int targetCount = 0; QRegExp rxClean(Project::instance()->markup() % '|' % Project::instance()->accel()); //cleaning regexp; NOTE isEmpty()? QRegExp rxSplit(QStringLiteral("\\W|\\d"));//splitting regexp DocPosition pos(0); do { QString msg = m_catalog->source(pos); msg.remove(rxClean); QStringList words = msg.split(rxSplit, QString::SkipEmptyParts); sourceCount += words.size(); msg = m_catalog->target(pos); msg.remove(rxClean); words = msg.split(rxSplit, QString::SkipEmptyParts); targetCount += words.size(); } while (switchNext(m_catalog, pos)); KMessageBox::information(this, i18nc("@info words count", "Source text words: %1
Target text words: %2", sourceCount, targetCount), i18nc("@title", "Word Count")); } bool EditorTab::findEntryBySourceContext(const QString& source, const QString& ctxt) { DocPosition pos(0); do { if (m_catalog->source(pos) == source && m_catalog->context(pos.entry) == QStringList(ctxt)) { gotoEntry(pos); return true; } } while (switchNext(m_catalog, pos)); return false; } //see also termlabel.h void EditorTab::defineNewTerm() { //TODO just a word under cursor? QString en(m_view->selectionInSource().toLower()); if (en.isEmpty()) en = m_catalog->msgid(m_currentPos).toLower(); QString target(m_view->selectionInTarget().toLower()); if (target.isEmpty()) target = m_catalog->msgstr(m_currentPos).toLower(); m_project->defineNewTerm(en, target); } void EditorTab::reloadFile() { QString mergeFilePath = m_syncView->filePath(); DocPosition p = m_currentPos; if (!fileOpen(currentFilePath())) return; gotoEntry(p); if (!mergeFilePath.isEmpty()) mergeOpen(mergeFilePath); } static void openLxrSearch(const QString& srcFileRelPath) { QDesktopServices::openUrl(QUrl(QStringLiteral("https://lxr.kde.org/search?_filestring=") + QString::fromLatin1(QUrl::toPercentEncoding(srcFileRelPath)))); } static void openLocalSource(const QString& file, int line) { if (Settings::self()->customEditorEnabled()) QProcess::startDetached(QString(Settings::self()->customEditorCommand()).arg(file).arg(line)); else QDesktopServices::openUrl(QUrl::fromLocalFile(file)); } void EditorTab::dispatchSrcFileOpenRequest(const QString& srcFileRelPath, int line) { // Try project scripts first. m_srcFileOpenRequestAccepted = false; emit srcFileOpenRequested(srcFileRelPath, line); if (m_srcFileOpenRequestAccepted) return; // If project scripts do not handle opening the source file, check if the // path exists relative to the current translation file path. QDir relativePath(currentFilePath()); relativePath.cdUp(); QString srcAbsolutePath(relativePath.absoluteFilePath(srcFileRelPath)); if (QFile::exists(srcAbsolutePath)) { openLocalSource(srcAbsolutePath, line); return; } QString dir = Project::instance()->local()->sourceDir(); if (dir.isEmpty()) { switch (KMessageBox::questionYesNoCancel(SettingsController::instance()->mainWindowPtr(), i18nc("@info", "Would you like to search for the source file locally or via lxr.kde.org?"), i18nc("@title:window", "Source file lookup"), KGuiItem(i18n("Locally")), KGuiItem("lxr.kde.org") )) { case KMessageBox::Yes: break; case KMessageBox::No: openLxrSearch(srcFileRelPath); case KMessageBox::Cancel: default: return; } } //hard fallback if (dir.isEmpty()) { dir = QFileDialog::getExistingDirectory(this, i18n("Select project's base folder for source file lookup"), QDir::homePath()); if (dir.length()) Project::instance()->local()->setSourceDir(dir); } if (dir.length()) { auto doOpen = [srcFileRelPath, line]() { auto sourceFilePaths = Project::instance()->sourceFilePaths(); bool found = false; QByteArray fn = srcFileRelPath.midRef(srcFileRelPath.lastIndexOf('/') + 1).toUtf8(); auto it = sourceFilePaths.constFind(fn); while (it != sourceFilePaths.constEnd() && it.key() == fn) { const QString absFilePath = QString::fromUtf8(it.value() + '/' + fn); if (!absFilePath.endsWith(srcFileRelPath) || !QFileInfo::exists(absFilePath)) { //for the case when link contained also folders it++; continue; } found = true; openLocalSource(absFilePath, line); it++; } if (!found) { switch (KMessageBox::warningYesNoCancel(SettingsController::instance()->mainWindowPtr(), i18nc("@info", "Could not find source file in the folder specified.\n" "Do you want to change source files folder?"), i18nc("@title:window", "Source file lookup"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KGuiItem(i18n("lxr.kde.org")))) { case KMessageBox::Cancel: Project::instance()->local()->setSourceDir(QString()); Project::instance()->resetSourceFilePaths(); openLxrSearch(srcFileRelPath); case KMessageBox::No: return; default: ; //fall through to dir selection } QString dir = QFileDialog::getExistingDirectory(0, i18n("Select project's base folder for source file lookup"), Project::instance()->local()->sourceDir()); if (dir.length()) { Project::instance()->local()->setSourceDir(dir); Project::instance()->resetSourceFilePaths(); } } }; if (!Project::instance()->sourceFilePaths().isEmpty()) doOpen(); else connect(Project::instance(), &Project::sourceFilePathsAreReady, doOpen); return; } // Otherwise, let the user know how to create a project script to handle // opening source file paths that are not relative to translation files. KMessageBox::information(this, i18nc("@info", "Cannot open the target source file: The target source file is not " "relative to the current translation file, and there are currently no " "scripts loaded to handle opening source files in custom paths. Refer " "to the Lokalize handbook for script examples and how to plug them " "into your project.")); } void EditorTab::mergeIntoOpenDocument() { if (!m_catalog || m_catalog->type() != Xliff) return; QString xliff2odf = QStringLiteral("xliff2odf"); if (QProcess::execute(xliff2odf, QStringList(QLatin1String("--version"))) == -2) { KMessageBox::error(SettingsController::instance()->mainWindowPtr(), i18n("Install translate-toolkit package and retry.")); return; } QString xliffFolder = QFileInfo(m_catalog->url()).absolutePath(); QString originalOdfFilePath = m_catalog->originalOdfFilePath(); if (originalOdfFilePath.isEmpty() || !QFile::exists(originalOdfFilePath)) { originalOdfFilePath = QFileDialog::getOpenFileName( SettingsController::instance()->mainWindowPtr(), i18n("Select original OpenDocument on which current XLIFF file is based"), xliffFolder, i18n("OpenDocument files (*.odt *.ods)")/*"text/x-lokalize-project"*/); if (originalOdfFilePath.length()) m_catalog->setOriginalOdfFilePath(originalOdfFilePath); } if (originalOdfFilePath.isEmpty()) return; saveFile(); //TODO check if odt did update (merge with new template is needed) QFileInfo originalOdfFileInfo(originalOdfFilePath); QString targetLangCode = m_catalog->targetLangCode(); QStringList args(m_catalog->url()); args.append(xliffFolder % '/' % originalOdfFileInfo.baseName() % '-' % targetLangCode % '.' % originalOdfFileInfo.suffix()); args.append(QStringLiteral("-t")); args.append(originalOdfFilePath); qCDebug(LOKALIZE_LOG) << args; QProcess::execute(xliff2odf, args); if (!QFile::exists(args.at(1))) return; //if (originalOdfFileInfo.suffix().toLower()==QLatin1String(".odt")) { QString lowriter = QStringLiteral("soffice"); if (QProcess::execute(lowriter, QStringList("--version")) == -2) { //TODO //KMessageBox::error(SettingsController::instance()->mainWindowPtr(), i18n("Install translate-toolkit package and retry")); return; } QProcess::startDetached(lowriter, QStringList(args.at(1))); QString reloaderScript = QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("scripts/odf/xliff2odf-standalone.py")); if (reloaderScript.length()) { QString python = QStringLiteral("python"); QStringList unoArgs(QStringLiteral("-c")); unoArgs.append(QStringLiteral("import uno")); if (QProcess::execute(python, unoArgs) != 0) { python = QStringLiteral("python3"); QStringList unoArgs(QStringLiteral("-c")); unoArgs.append(QStringLiteral("import uno")); if (QProcess::execute(python, unoArgs) != 0) { KMessageBox::information(SettingsController::instance()->mainWindowPtr(), i18n("Install python-uno package for additional functionality.")); return; } } QStringList reloaderArgs(reloaderScript); reloaderArgs.append(args.at(1)); reloaderArgs.append(currentEntryId()); QProcess::execute(python, reloaderArgs); } } } //BEGIN DBus interface #include "editoradaptor.h" QList EditorTab::ids; QString EditorTab::dbusObjectPath() { const QString EDITOR_PATH = QStringLiteral("/ThisIsWhatYouWant/Editor/"); if (m_dbusId == -1) { m_adaptor = new EditorAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(EDITOR_PATH + QString::number(m_dbusId), this); } return EDITOR_PATH + QString::number(m_dbusId); } QString EditorTab::currentFilePath() { return m_catalog->url(); } QByteArray EditorTab::currentFileContents() { return m_catalog->contents(); } QString EditorTab::currentEntryId() { return m_catalog->id(m_currentPos); } QString EditorTab::selectionInTarget() { return m_view->selectionInTarget(); } QString EditorTab::selectionInSource() { return m_view->selectionInSource(); } void EditorTab::lookupSelectionInTranslationMemory() { emit tmLookupRequested(selectionInSource(), selectionInTarget()); } void EditorTab::setEntryFilteredOut(int entry, bool filteredOut) { m_transUnitsView->setEntryFilteredOut(entry, filteredOut); } void EditorTab::setEntriesFilteredOut(bool filteredOut) { m_transUnitsView->setEntriesFilteredOut(filteredOut); } int EditorTab::entryCount() { return m_catalog->numberOfEntries(); } QString EditorTab::entrySource(int entry, int form) { return m_catalog->sourceWithTags(DocPosition(entry, form)).string; } QString EditorTab::entryTarget(int entry, int form) { return m_catalog->targetWithTags(DocPosition(entry, form)).string; } int EditorTab::entryPluralFormCount(int entry) { return m_catalog->isPlural(entry) ? m_catalog->numberOfPluralForms() : 1; } bool EditorTab::entryReady(int entry) { return m_catalog->isApproved(entry); } QString EditorTab::sourceLangCode() { return m_catalog->sourceLangCode(); } QString EditorTab::targetLangCode() { return m_catalog->targetLangCode(); } void EditorTab::addEntryNote(int entry, const QString& note) { m_notesView->addNote(entry, note); } void EditorTab::addTemporaryEntryNote(int entry, const QString& note) { m_notesView->addTemporaryEntryNote(entry, note); } void EditorTab::addAlternateTranslation(int entry, const QString& translation) { m_altTransView->addAlternateTranslation(entry, translation); } void EditorTab::addTemporaryAlternateTranslation(int entry, const QString& translation) { m_altTransView->addAlternateTranslation(entry, translation); } void EditorTab::attachAlternateTranslationFile(const QString& path) { m_altTransView->attachAltTransFile(path); } void EditorTab::setEntryTarget(int entry, int form, const QString& content) { DocPosition pos(entry, form); m_catalog->beginMacro(i18nc("@item Undo action item", "Set unit text")); removeTargetSubstring(m_catalog, pos); insertCatalogString(m_catalog, pos, CatalogString(content)); m_catalog->endMacro(); if (m_currentPos == pos) m_view->gotoEntry(); } //END DBus interface diff --git a/src/editortab.h b/src/editortab.h index d62e51b..150e61d 100644 --- a/src/editortab.h +++ b/src/editortab.h @@ -1,406 +1,407 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef EDITORTAB_H #define EDITORTAB_H #ifdef HAVE_CONFIG_H #include #endif #include "pos.h" #include "lokalizesubwindowbase.h" #include #include namespace Sonnet { class Dialog; } namespace Sonnet { class BackgroundChecker; } #include class KFind; class KReplace; class KActionCategory; class Project; class Catalog; class EditorView; class MergeView; class CatalogView; class MsgCtxtView; class AltTransView; namespace GlossaryNS { class GlossaryView; } struct EditorState { public: EditorState(): entry(0) {} EditorState(const EditorState& s): dockWidgets(s.dockWidgets), filePath(s.filePath), entry(0) {} ~EditorState() {} QByteArray dockWidgets; QString filePath; QString mergeFilePath; int entry; //int offset; }; /** * @short Editor tab * * This class can be called a dispatcher for one message catalog. * * It is accessible via Lokalize.currentEditor() from kross scripts and via * '/ThisIsWhatYouWant/Editor/# : org.kde.Lokalize.Editor' from qdbusviewer * * @author Nick Shaforostoff */ class EditorTab: public LokalizeSubwindowBase2 { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.Editor") //qdbuscpp2xml -m -s editortab.h -o org.kde.lokalize.Editor.xml #define qdbuscpp2xml public: explicit EditorTab(QWidget* parent, bool valid = true); ~EditorTab() override; //interface for LokalizeMainWindow void hideDocks() override; void showDocks() override; QString currentFilePath() override; void setFullPathShown(bool); void setProperCaption(QString, bool); //reimpl to remove ' - Lokalize' public slots: void setProperFocus(); public: bool queryClose() override; EditorState state(); KXMLGUIClient* guiClient() override { return (KXMLGUIClient*)this; } QString dbusObjectPath(); int dbusId() { return m_dbusId; } QObject* adaptor() { return m_adaptor; } //wrapper for cmdline handling void mergeOpen(QString mergeFilePath); bool fileOpen(QString filePath = QString(), QString suggestedDirPath = QString(), bool silent = false); public slots: //for undo/redo, views void gotoEntry(DocPosition pos); void gotoEntry(DocPosition pos, int selection); #ifdef qdbuscpp2xml Q_SCRIPTABLE void gotoEntry(int entry) { gotoEntry(DocPosition(entry)); } Q_SCRIPTABLE void gotoEntryForm(int entry, int form) { gotoEntry(DocPosition(entry, form)); } Q_SCRIPTABLE void gotoEntryFormOffset(int entry, int form, int offset) { gotoEntry(DocPosition(entry, form, offset)); } Q_SCRIPTABLE void gotoEntryFormOffsetSelection(int entry, int form, int offset, int selection) { gotoEntry(DocPosition(entry, form, offset), selection); } Q_SCRIPTABLE QString currentEntryId(); Q_SCRIPTABLE int currentEntry() { return m_currentPos.entry; } Q_SCRIPTABLE int currentForm() { return m_currentPos.form; } Q_SCRIPTABLE QString selectionInTarget(); Q_SCRIPTABLE QString selectionInSource(); Q_SCRIPTABLE void setEntryFilteredOut(int entry, bool filteredOut); Q_SCRIPTABLE void setEntriesFilteredOut(bool filteredOut); Q_SCRIPTABLE int entryCount(); Q_SCRIPTABLE QString entrySource(int entry, int form); Q_SCRIPTABLE QString entryTarget(int entry, int form); Q_SCRIPTABLE void setEntryTarget(int entry, int form, const QString& content); Q_SCRIPTABLE int entryPluralFormCount(int entry); Q_SCRIPTABLE bool entryReady(int entry); Q_SCRIPTABLE void addEntryNote(int entry, const QString& note); Q_SCRIPTABLE void addTemporaryEntryNote(int entry, const QString& note); Q_SCRIPTABLE void addAlternateTranslation(int entry, const QString& translation); Q_SCRIPTABLE void addTemporaryAlternateTranslation(int entry, const QString& translation); Q_SCRIPTABLE QString currentFile() { return currentFilePath(); } Q_SCRIPTABLE QByteArray currentFileContents(); Q_SCRIPTABLE QString sourceLangCode(); Q_SCRIPTABLE QString targetLangCode(); Q_SCRIPTABLE void attachAlternateTranslationFile(const QString& path); Q_SCRIPTABLE void openSyncSource(QString path) { mergeOpen(path); } Q_SCRIPTABLE void reloadFile(); #endif Q_SCRIPTABLE bool saveFile(const QString& filePath = QString()); Q_SCRIPTABLE bool saveFileAs(const QString& defaultPath = QString()); Q_SCRIPTABLE void close() { return parent()->deleteLater(); } Q_SCRIPTABLE void gotoNextUnfiltered(); Q_SCRIPTABLE void gotoPrevUnfiltered(); Q_SCRIPTABLE void gotoFirstUnfiltered(); Q_SCRIPTABLE void gotoLastUnfiltered(); Q_SCRIPTABLE void gotoNext(); Q_SCRIPTABLE void gotoPrev(); Q_SCRIPTABLE void gotoFirst(); Q_SCRIPTABLE void gotoLast(); Q_SCRIPTABLE void mergeIntoOpenDocument(); Q_SCRIPTABLE bool findEntryBySourceContext(const QString& source, const QString& ctxt); Q_SCRIPTABLE bool isValid() { return m_valid; } Q_SCRIPTABLE void setSrcFileOpenRequestAccepted(bool a) { m_srcFileOpenRequestAccepted = a; } private slots: void highlightFound(const QString &, int, int); //for find/replace void highlightFound_(const QString &, int, int); //for find/replace void lookupSelectionInTranslationMemory(); //statusbar indication void numberOfFuzziesChanged(); void numberOfUntranslatedChanged(); //fuzzy, untr [statusbar] indication void msgStrChanged(); //modif [caption] indication void setModificationSign(); void updateCaptionPath(); //gui void switchForm(int); void undo(); void redo(); void findNext(); void findPrev(); void find(); void replace(); void replaceNext();//internal void doReplace(const QString&, int, int, int); //internal void cleanupReplace();//internal // void selectAll(); // void deselectAll(); // void clear(); // void search2msgstr(); // void plural2msgstr(); void gotoEntry(); void gotoPrevFuzzyUntr(); bool gotoNextFuzzyUntr(const DocPosition& pos); bool gotoNextFuzzyUntr(); void gotoNextFuzzy(); void gotoPrevFuzzy(); void gotoNextUntranslated(); void gotoPrevUntranslated(); void toggleApprovementGotoNextFuzzyUntr(); void setApproveActionTitle(); void spellcheck(); void spellcheckNext(); void spellcheckShow(const QString&, int); void spellcheckReplace(QString, int, const QString&); void spellcheckStop(); void spellcheckCancel(); void gotoNextBookmark(); void gotoPrevBookmark(); void displayWordCount(); void clearTranslatedEntries(); void launchPology(); void openPhasesWindow(); void defineNewTerm(); void initLater(); void showStatesMenu(); void setState(QAction*); void dispatchSrcFileOpenRequest(const QString& srcPath, int line); void indexWordsForCompletion(); void fileAutoSaveFailedWarning(const QString&); void pologyHasFinished(int exitCode, QProcess::ExitStatus exitStatus); protected: void paintEvent(QPaintEvent* event) override; private: void setupAccel(); void setupActions(); void setupStatusBar(); void findNext(const DocPosition& startingPos); void replaceNext(const DocPosition&); private: Project* m_project; Catalog* m_catalog; EditorView* m_view; QAction* m_approveAndGoAction; QAction* m_approveAction; QAction* m_stateAction; KProcess* m_pologyProcess; bool m_pologyProcessInProgress; DocPosition m_currentPos; DocPosition _searchingPos; //for find/replace DocPosition _replacingPos; DocPosition _spellcheckPos; DocPosition _spellcheckStartPos; Sonnet::BackgroundChecker* m_sonnetChecker; Sonnet::Dialog* m_sonnetDialog; int m_spellcheckStartUndoIndex; bool m_spellcheckStop: 1; bool m_currentIsApproved: 1; //for statusbar animation bool m_currentIsUntr: 1; //for statusbar animation bool m_fullPathShown: 1; bool m_doReplaceCalled: 1; //used to prevent non-clean catalog status KFind* m_find; KReplace* m_replace; //BEGIN views MergeView* m_syncView; MergeView* m_syncViewSecondary; CatalogView* m_transUnitsView; MsgCtxtView* m_notesView; AltTransView* m_altTransView; //END views QString _captionPath; bool m_srcFileOpenRequestAccepted; //BEGIN dbus bool m_valid; QObject* m_adaptor; int m_dbusId; static QList ids; //END dbus signals: void tmLookupRequested(DocPosition::Part, const QString&); void tmLookupRequested(const QString& source, const QString& target); Q_SCRIPTABLE void srcFileOpenRequested(const QString& srcPath, int line); void fileOpenRequested(const QString& filePath, const QString& str, const QString& ctxt, const bool setAsActive); //emitted when mainwindow is closed or another file is opened void fileClosed(); Q_SCRIPTABLE void fileClosed(const QString& path); Q_SCRIPTABLE void fileSaved(const QString& path); Q_SCRIPTABLE void fileAboutToBeClosed();//old catalog is still accessible Q_SCRIPTABLE void fileOpened(); Q_SCRIPTABLE void entryDisplayed(); void signalNewEntryDisplayed(const DocPosition&); void signalEntryWithMergeDisplayed(bool, const DocPosition&); void signalFirstDisplayed(bool); void signalLastDisplayed(bool); void signalEquivTranslatedEntryDisplayed(bool); void signalApprovedEntryDisplayed(bool); void signalFuzzyEntryDisplayed(bool); void signalPriorFuzzyAvailable(bool); void signalNextFuzzyAvailable(bool); void signalPriorUntranslatedAvailable(bool); void signalNextUntranslatedAvailable(bool); void signalPriorFuzzyOrUntrAvailable(bool); void signalNextFuzzyOrUntrAvailable(bool); // merge mode signals gone to the view //NOTE move these to catalog tree view? void signalPriorBookmarkAvailable(bool); void signalNextBookmarkAvailable(bool); void signalBookmarkDisplayed(bool); Q_SCRIPTABLE void xliffFileOpened(bool); }; #endif diff --git a/src/editortab_findreplace.cpp b/src/editortab_findreplace.cpp index 5561cfc..40ca12e 100644 --- a/src/editortab_findreplace.cpp +++ b/src/editortab_findreplace.cpp @@ -1,618 +1,619 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalize_debug.h" #include "editortab.h" #include "editorview.h" #include "catalog.h" #include "pos.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "ui_kaider_findextension.h" #include "stemming.h" #include #include #include #include #include #include #include #include #define IGNOREACCELS KFind::MinimumUserOption #define INCLUDENOTES KFind::MinimumUserOption*2 static long makeOptions(long options, const Ui_findExtension* ui_findExtension) { return options + IGNOREACCELS * ui_findExtension->m_ignoreAccelMarks->isChecked() + INCLUDENOTES * ui_findExtension->m_notes->isChecked(); //bool skipMarkup(){return ui_findExtension->m_skipTags->isChecked();} } class EntryFindDialog: public KFindDialog { public: EntryFindDialog(QWidget* parent); ~EntryFindDialog(); long options() const { return makeOptions(KFindDialog::options(), ui_findExtension); } static EntryFindDialog* instance(QWidget* parent = nullptr); private: static QPointer _instance; static void cleanup() { delete EntryFindDialog::_instance; } private: Ui_findExtension* ui_findExtension; }; QPointer EntryFindDialog::_instance = 0; EntryFindDialog* EntryFindDialog::instance(QWidget* parent) { if (_instance == 0) { _instance = new EntryFindDialog(parent); qAddPostRoutine(EntryFindDialog::cleanup); } return _instance; } EntryFindDialog::EntryFindDialog(QWidget* parent) : KFindDialog(parent) , ui_findExtension(new Ui_findExtension) { ui_findExtension->setupUi(findExtension()); setHasSelection(false); KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); setOptions(stateGroup.readEntry("FindOptions", (qlonglong)0)); setFindHistory(stateGroup.readEntry("FindHistory", QStringList())); } EntryFindDialog::~EntryFindDialog() { KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); stateGroup.writeEntry("FindOptions", (qlonglong)options()); stateGroup.writeEntry("FindHistory", findHistory()); delete ui_findExtension; } //BEGIN EntryReplaceDialog class EntryReplaceDialog: public KReplaceDialog { public: EntryReplaceDialog(QWidget* parent); ~EntryReplaceDialog(); long options() const { return makeOptions(KReplaceDialog::options(), ui_findExtension); } static EntryReplaceDialog* instance(QWidget* parent = nullptr); private: static QPointer _instance; static void cleanup() { delete EntryReplaceDialog::_instance; } private: Ui_findExtension* ui_findExtension; }; QPointer EntryReplaceDialog::_instance = 0; EntryReplaceDialog* EntryReplaceDialog::instance(QWidget* parent) { if (_instance == 0) { _instance = new EntryReplaceDialog(parent); qAddPostRoutine(EntryReplaceDialog::cleanup); } return _instance; } EntryReplaceDialog::EntryReplaceDialog(QWidget* parent) : KReplaceDialog(parent) , ui_findExtension(new Ui_findExtension) { ui_findExtension->setupUi(findExtension()); //ui_findExtension->m_notes->hide(); setHasSelection(false); KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); setOptions(stateGroup.readEntry("ReplaceOptions", (qlonglong)0)); setFindHistory(stateGroup.readEntry("ReplacePatternHistory", QStringList())); setReplacementHistory(stateGroup.readEntry("ReplacementHistory", QStringList())); } EntryReplaceDialog::~EntryReplaceDialog() { KConfig config; KConfigGroup stateGroup(&config, "FindReplace"); stateGroup.writeEntry("ReplaceOptions", (qlonglong)options()); stateGroup.writeEntry("ReplacePatternHistory", findHistory()); stateGroup.writeEntry("ReplacementHistory", replacementHistory()); delete ui_findExtension; } //END EntryReplaceDialog //TODO &,   static void calcOffsetWithAccels(const QString& data, int& offset, int& length) { int i = 0; for (; i < offset; ++i) if (Q_UNLIKELY(data.at(i) == '&')) ++offset; //if & is inside highlighted word int limit = offset + length; for (i = offset; i < limit; ++i) if (Q_UNLIKELY(data.at(i) == '&')) { ++length; limit = qMin(data.size(), offset + length); //just safety } } static bool determineStartingPos(Catalog* m_catalog, KFind* find, DocPosition& pos) //search or replace //called from find() and findNext() { if (find->options() & KFind::FindBackwards) { pos.entry = m_catalog->numberOfEntries() - 1; pos.form = (m_catalog->isPlural(pos.entry)) ? m_catalog->numberOfPluralForms() - 1 : 0; } else { pos.entry = 0; pos.form = 0; } return true; } void EditorTab::find() { //QWidget* p=0; QWidget* next=qobject_cast(parent()); while(next) { p=next; next=qobject_cast(next->parent()); } EntryFindDialog::instance(nativeParentWidget()); QString sel = selectionInTarget(); if (!(sel.isEmpty() && selectionInSource().isEmpty())) { if (sel.isEmpty()) sel = selectionInSource(); if (m_find && m_find->options()&IGNOREACCELS) sel.remove('&'); EntryFindDialog::instance()->setPattern(sel); } if (EntryFindDialog::instance()->exec() != QDialog::Accepted) return; if (m_find) { m_find->resetCounts(); m_find->setPattern(EntryFindDialog::instance()->pattern()); m_find->setOptions(EntryFindDialog::instance()->options()); } else { // This creates a find-next-prompt dialog if needed. m_find = new KFind(EntryFindDialog::instance()->pattern(), EntryFindDialog::instance()->options(), this, EntryFindDialog::instance()); connect(m_find, QOverload::of(&KFind::highlight), this, &EditorTab::highlightFound); connect(m_find, &KFind::findNext, this, QOverload<>::of(&EditorTab::findNext)); m_find->closeFindNextDialog(); } DocPosition pos; if (m_find->options() & KFind::FromCursor) pos = m_currentPos; else if (!determineStartingPos(m_catalog, m_find, pos)) return; findNext(pos); } void EditorTab::findNext(const DocPosition& startingPos) { Catalog& catalog = *m_catalog; KFind& find = *m_find; if (Q_UNLIKELY(catalog.numberOfEntries() <= startingPos.entry)) return;//for the case when app wasn't able to process event before file close bool anotherEntry = _searchingPos.entry != m_currentPos.entry; _searchingPos = startingPos; if (anotherEntry) _searchingPos.offset = 0; QRegExp rx("[^(\\\\n)>]\n"); QTime a; a.start(); //_searchingPos.part=DocPosition::Source; bool ignoreaccels = m_find->options()&IGNOREACCELS; bool includenotes = m_find->options()&INCLUDENOTES; int switchOptions = DocPosition::Source | DocPosition::Target | (includenotes * DocPosition::Comment); int flag = 1; while (flag) { flag = 0; KFind::Result res = KFind::NoMatch; while (true) { if (find.needData() || anotherEntry || m_view->m_modifiedAfterFind) { anotherEntry = false; m_view->m_modifiedAfterFind = false; QString data; if (_searchingPos.part == DocPosition::Comment) data = catalog.notes(_searchingPos).at(_searchingPos.form).content; else data = catalog.catalogString(_searchingPos).string; if (ignoreaccels) data.remove('&'); find.setData(data); } res = find.find(); //offset=-1; if (res != KFind::NoMatch) break; if (!( (find.options()&KFind::FindBackwards) ? switchPrev(m_catalog, _searchingPos, switchOptions) : switchNext(m_catalog, _searchingPos, switchOptions) )) break; } if (res == KFind::NoMatch) { //file-wide search if (find.shouldRestart(true, true)) { flag = 1; determineStartingPos(m_catalog, m_find, _searchingPos); } find.resetCounts(); } } } void EditorTab::findNext() { if (m_find) { findNext((m_currentPos.entry == _searchingPos.entry && _searchingPos.part == DocPosition::Comment) ? _searchingPos : m_currentPos); } else find(); } void EditorTab::findPrev() { if (m_find) { m_find->setOptions(m_find->options() ^ KFind::FindBackwards); findNext(m_currentPos); } else { find(); } } void EditorTab::highlightFound(const QString &, int matchingIndex, int matchedLength) { if (m_find->options()&IGNOREACCELS && _searchingPos.part != DocPosition::Comment) { QString data = m_catalog->catalogString(_searchingPos).string; calcOffsetWithAccels(data, matchingIndex, matchedLength); } _searchingPos.offset = matchingIndex; gotoEntry(_searchingPos, matchedLength); } void EditorTab::replace() { EntryReplaceDialog::instance(nativeParentWidget()); if (!m_view->selectionInTarget().isEmpty()) { if (m_replace && m_replace->options()&IGNOREACCELS) { QString tmp(m_view->selectionInTarget()); tmp.remove('&'); EntryReplaceDialog::instance()->setPattern(tmp); } else EntryReplaceDialog::instance()->setPattern(m_view->selectionInTarget()); } if (EntryReplaceDialog::instance()->exec() != QDialog::Accepted) return; if (m_replace) m_replace->deleteLater();// _replace=0; // This creates a find-next-prompt dialog if needed. { m_replace = new KReplace(EntryReplaceDialog::instance()->pattern(), EntryReplaceDialog::instance()->replacement(), EntryReplaceDialog::instance()->options(), this, EntryReplaceDialog::instance()); connect(m_replace, QOverload::of(&KReplace::highlight), this, &EditorTab::highlightFound_); connect(m_replace, &KReplace::findNext, this, QOverload<>::of(&EditorTab::replaceNext)); connect(m_replace, QOverload::of(&KReplace::replace), this, &EditorTab::doReplace); connect(m_replace, &KReplace::dialogClosed, this, &EditorTab::cleanupReplace); // _replace->closeReplaceNextDialog(); } // else // { // _replace->resetCounts(); // _replace->setPattern(EntryReplaceDialog::instance()->pattern()); // _replace->setOptions(EntryReplaceDialog::instance()->options()); // } //m_catalog->beginMacro(i18nc("@item Undo action item","Replace")); m_doReplaceCalled = false; if (m_replace->options() & KFind::FromCursor) replaceNext(m_currentPos); else { DocPosition pos; if (!determineStartingPos(m_catalog, m_replace, pos)) return; replaceNext(pos); } } void EditorTab::replaceNext(const DocPosition& startingPos) { bool anotherEntry = m_currentPos.entry != _replacingPos.entry; _replacingPos = startingPos; if (anotherEntry) _replacingPos.offset = 0; int flag = 1; bool ignoreaccels = m_replace->options()&IGNOREACCELS; bool includenotes = m_replace->options()&INCLUDENOTES; qCWarning(LOKALIZE_LOG) << "includenotes" << includenotes; int switchOptions = DocPosition::Target | (includenotes * DocPosition::Comment); while (flag) { flag = 0; KFind::Result res = KFind::NoMatch; while (1) { if (m_replace->needData() || anotherEntry/*||m_view->m_modifiedAfterFind*/) { anotherEntry = false; //m_view->m_modifiedAfterFind=false;//NOTE TEST THIS QString data; if (_replacingPos.part == DocPosition::Comment) data = m_catalog->notes(_replacingPos).at(_replacingPos.form).content; else { data = m_catalog->targetWithTags(_replacingPos).string; if (ignoreaccels) data.remove('&'); } m_replace->setData(data); } res = m_replace->replace(); if (res != KFind::NoMatch) break; if (!( (m_replace->options()&KFind::FindBackwards) ? switchPrev(m_catalog, _replacingPos, switchOptions) : switchNext(m_catalog, _replacingPos, switchOptions) )) break; } if (res == KFind::NoMatch) { if ((m_replace->options()&KFind::FromCursor) && m_replace->shouldRestart(true)) { flag = 1; determineStartingPos(m_catalog, m_replace, _replacingPos); } else { if (!(m_replace->options() & KFind::FromCursor)) m_replace->displayFinalDialog(); m_replace->closeReplaceNextDialog(); cleanupReplace(); } m_replace->resetCounts(); } } } void EditorTab::cleanupReplace() { if (m_doReplaceCalled) { m_doReplaceCalled = false; m_catalog->endMacro(); } } void EditorTab::replaceNext() { replaceNext(m_currentPos); } void EditorTab::highlightFound_(const QString &, int matchingIndex, int matchedLength) { if (m_replace->options()&IGNOREACCELS) { QString data = m_catalog->targetWithTags(_replacingPos).string; calcOffsetWithAccels(data, matchingIndex, matchedLength); } _replacingPos.offset = matchingIndex; gotoEntry(_replacingPos, matchedLength); } void EditorTab::doReplace(const QString &newStr, int offset, int newLen, int remLen) { if (!m_doReplaceCalled) { m_doReplaceCalled = true; m_catalog->beginMacro(i18nc("@item Undo action item", "Replace")); } DocPosition pos = _replacingPos; if (_replacingPos.part == DocPosition::Comment) m_catalog->push(new SetNoteCmd(m_catalog, pos, newStr)); else { QString oldStr = m_catalog->target(_replacingPos); if (m_replace->options()&IGNOREACCELS) calcOffsetWithAccels(oldStr, offset, remLen); pos.offset = offset; m_catalog->push(new DelTextCmd(m_catalog, pos, oldStr.mid(offset, remLen))); if (newLen) m_catalog->push(new InsTextCmd(m_catalog, pos, newStr.mid(offset, newLen))); } if (pos.entry == m_currentPos.entry) { pos.offset += newLen; m_view->gotoEntry(pos, 0); } } void EditorTab::spellcheck() { if (!m_sonnetDialog) { m_sonnetChecker = new Sonnet::BackgroundChecker(this); m_sonnetChecker->changeLanguage(enhanceLangCode(Project::instance()->langCode())); m_sonnetDialog = new Sonnet::Dialog(m_sonnetChecker, this); connect(m_sonnetDialog, QOverload::of(&Sonnet::Dialog::done), this, &EditorTab::spellcheckNext); connect(m_sonnetDialog, &Sonnet::Dialog::replace, this, &EditorTab::spellcheckReplace); connect(m_sonnetDialog, &Sonnet::Dialog::stop, this, &EditorTab::spellcheckStop); connect(m_sonnetDialog, &Sonnet::Dialog::cancel, this, &EditorTab::spellcheckCancel); connect(m_sonnetDialog/*m_sonnetChecker*/, &Sonnet::Dialog::misspelling, this, &EditorTab::spellcheckShow); // disconnect(/*m_sonnetDialog*/m_sonnetChecker,SIGNAL(misspelling(QString,int)), // m_sonnetDialog,SLOT(slotMisspelling(QString,int))); // // connect( d->checker, SIGNAL(misspelling(const QString&, int)), // SLOT(slotMisspelling(const QString&, int)) ); } QString text = m_catalog->msgstr(m_currentPos); if (!m_view->selectionInTarget().isEmpty()) text = m_view->selectionInTarget(); text.remove('&'); m_sonnetDialog->setBuffer(text); _spellcheckPos = m_currentPos; _spellcheckStartPos = m_currentPos; m_spellcheckStop = false; //m_catalog->beginMacro(i18n("Spellcheck")); m_spellcheckStartUndoIndex = m_catalog->index(); m_sonnetDialog->show(); } void EditorTab::spellcheckNext() { if (m_spellcheckStop) return; do { if (!switchNext(m_catalog, _spellcheckPos)) { qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.entry; qCDebug(LOKALIZE_LOG) << _spellcheckStartPos.form; bool continueFromStart = !(_spellcheckStartPos.entry == 0 && _spellcheckStartPos.form == 0) && KMessageBox::questionYesNo(this, i18n("Lokalize has reached end of document. Do you want to continue from start?"), i18nc("@title", "Spellcheck")) == KMessageBox::Yes; if (continueFromStart) { _spellcheckStartPos.entry = 0; _spellcheckStartPos.form = 0; _spellcheckPos = _spellcheckStartPos; } else { KMessageBox::information(this, i18n("Lokalize has finished spellchecking"), i18nc("@title", "Spellcheck")); return; } } } while (m_catalog->msgstr(_spellcheckPos).isEmpty() || !m_catalog->isApproved(_spellcheckPos.entry)); m_sonnetDialog->setBuffer(m_catalog->msgstr(_spellcheckPos).remove(Project::instance()->accel())); } void EditorTab::spellcheckStop() { m_spellcheckStop = true; } void EditorTab::spellcheckCancel() { m_catalog->setIndex(m_spellcheckStartUndoIndex); gotoEntry(_spellcheckPos); } void EditorTab::spellcheckShow(const QString &word, int offset) { const Project& project = *Project::instance(); const QString accel = project.accel(); QString source = m_catalog->source(_spellcheckPos); source.remove(accel); if (source.contains(word) && project.targetLangCode().leftRef(2) != project.sourceLangCode().leftRef(2)) { m_sonnetDialog->setUpdatesEnabled(false); m_sonnetChecker->continueChecking(); return; } m_sonnetDialog->setUpdatesEnabled(true); show(); DocPosition pos = _spellcheckPos; int length = word.length(); calcOffsetWithAccels(m_catalog->target(pos), offset, length); pos.offset = offset; gotoEntry(pos, length); } void EditorTab::spellcheckReplace(QString oldWord, int offset, const QString &newWord) { DocPosition pos = _spellcheckPos; int length = oldWord.length(); calcOffsetWithAccels(m_catalog->target(pos), offset, length); pos.offset = offset; if (length > oldWord.length()) //replaced word contains accel mark oldWord = m_catalog->target(pos).mid(offset, length); m_catalog->push(new DelTextCmd(m_catalog, pos, oldWord)); m_catalog->push(new InsTextCmd(m_catalog, pos, newWord)); gotoEntry(pos, newWord.length()); } diff --git a/src/editorview.cpp b/src/editorview.cpp index 80ddcdf..3de30be 100644 --- a/src/editorview.cpp +++ b/src/editorview.cpp @@ -1,354 +1,355 @@ /* **************************************************************************** This file is part of Lokalize (some bits of KBabel code were reused) Copyright (C) 2007-2014 by Nick Shaforostoff Copyright (C) 1999-2000 by Matthias Kiefer 2001-2004 by Stanislav Visnovsky + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "editorview.h" #include "lokalize_debug.h" #include "xlifftextedit.h" #include "project.h" #include "catalog.h" #include "cmd.h" #include "prefs_lokalize.h" #include "prefs.h" #include #include #include #include #include #include #include #include #include #include #include #include //parent is set on qsplitter insertion LedsWidget::LedsWidget(QWidget* parent): QWidget(parent) { KColorScheme colorScheme(QPalette::Normal); QHBoxLayout* layout = new QHBoxLayout(this); layout->addStretch(); layout->addWidget(new QLabel(i18nc("@label whether entry is fuzzy", "Not ready:"), this)); layout->addWidget(ledFuzzy = new KLed(colorScheme.foreground(KColorScheme::NeutralText).color()/*Qt::green*/, KLed::Off, KLed::Sunken, KLed::Rectangular)); layout->addWidget(new QLabel(i18nc("@label whether entry is untranslated", "Untranslated:"), this)); layout->addWidget(ledUntr = new KLed(colorScheme.foreground(KColorScheme::NegativeText).color()/*Qt::red*/, KLed::Off, KLed::Sunken, KLed::Rectangular)); layout->addSpacing(1); layout->addWidget(lblColumn = new QLabel(this)); layout->addStretch(); setMaximumHeight(minimumSizeHint().height()); } void LedsWidget::contextMenuEvent(QContextMenuEvent* event) { QMenu menu; menu.addAction(i18nc("@action", "Hide")); if (!menu.exec(event->globalPos())) return; //NOTE the config doesn't seem to work Settings::setLeds(false); SettingsController::instance()->dirty = true; hide(); } void LedsWidget::cursorPositionChanged(int column) { lblColumn->setText(i18nc("@info:label cursor position", "Column: %1", column)); } EditorView::EditorView(QWidget *parent, Catalog* catalog/*,keyEventHandler* kh*/) : QSplitter(Qt::Vertical, parent) , m_catalog(catalog) , m_sourceTextEdit(new TranslationUnitTextEdit(catalog, DocPosition::Source, this)) , m_targetTextEdit(new TranslationUnitTextEdit(catalog, DocPosition::Target, this)) , m_pluralTabBar(new QTabBar(this)) , m_leds(0) , m_modifiedAfterFind(false) { m_pluralTabBar->hide(); m_sourceTextEdit->setWhatsThis(i18n("

Original String

\n" "

This part of the window shows the original message\n" "of the currently displayed entry.

")); m_sourceTextEdit->viewport()->setBackgroundRole(QPalette::Background); m_sourceTextEdit->setVisualizeSeparators(Settings::self()->visualizeSeparators()); m_targetTextEdit->setVisualizeSeparators(Settings::self()->visualizeSeparators()); connect(m_targetTextEdit, &TranslationUnitTextEdit::contentsModified, this, &EditorView::resetFindForCurrent); connect(m_targetTextEdit, &TranslationUnitTextEdit::toggleApprovementRequested, this, &EditorView::toggleApprovement); connect(this, &EditorView::signalApprovedEntryDisplayed, m_targetTextEdit, &TranslationUnitTextEdit::reflectApprovementState); connect(m_sourceTextEdit, &TranslationUnitTextEdit::tagInsertRequested, m_targetTextEdit, &TranslationUnitTextEdit::insertTag); connect(m_sourceTextEdit, &TranslationUnitTextEdit::binaryUnitSelectRequested, this, &EditorView::binaryUnitSelectRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::binaryUnitSelectRequested, this, &EditorView::binaryUnitSelectRequested); connect(m_sourceTextEdit, &TranslationUnitTextEdit::gotoEntryRequested, this, &EditorView::gotoEntryRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::gotoEntryRequested, this, &EditorView::gotoEntryRequested); connect(m_sourceTextEdit, &TranslationUnitTextEdit::tmLookupRequested, this, &EditorView::tmLookupRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::tmLookupRequested, this, &EditorView::tmLookupRequested); connect(m_sourceTextEdit, &TranslationUnitTextEdit::findRequested, this, &EditorView::findRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::findRequested, this, &EditorView::findRequested); connect(m_sourceTextEdit, &TranslationUnitTextEdit::findNextRequested, this, &EditorView::findNextRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::findNextRequested, this, &EditorView::findNextRequested); connect(m_sourceTextEdit, &TranslationUnitTextEdit::replaceRequested, this, &EditorView::replaceRequested); connect(m_targetTextEdit, &TranslationUnitTextEdit::replaceRequested, this, &EditorView::replaceRequested); connect(this, &EditorView::doExplicitCompletion, m_targetTextEdit, &TranslationUnitTextEdit::doExplicitCompletion); addWidget(m_pluralTabBar); addWidget(m_sourceTextEdit); addWidget(m_targetTextEdit); QWidget::setTabOrder(m_targetTextEdit, m_sourceTextEdit); QWidget::setTabOrder(m_sourceTextEdit, m_targetTextEdit); setFocusProxy(m_targetTextEdit); // QTimer::singleShot(3000,this,SLOT(setupWhatsThis())); settingsChanged(); } EditorView::~EditorView() { } void EditorView::resetFindForCurrent(const DocPosition& pos) { m_modifiedAfterFind = true; emit signalChanged(pos.entry); } void EditorView::settingsChanged() { //Settings::self()->config()->setGroup("Editor"); m_sourceTextEdit->document()->setDefaultFont(Settings::msgFont()); m_targetTextEdit->document()->setDefaultFont(Settings::msgFont()); m_sourceTextEdit->setVisualizeSeparators(Settings::self()->visualizeSeparators()); m_targetTextEdit->setVisualizeSeparators(Settings::self()->visualizeSeparators()); if (m_leds) m_leds->setVisible(Settings::leds()); else if (Settings::leds()) { m_leds = new LedsWidget(this); insertWidget(2, m_leds); connect(m_targetTextEdit, &TranslationUnitTextEdit::cursorPositionChanged, m_leds, &LedsWidget::cursorPositionChanged); connect(m_targetTextEdit, &TranslationUnitTextEdit::nonApprovedEntryDisplayed, m_leds->ledFuzzy, &KLed::on); connect(m_targetTextEdit, &TranslationUnitTextEdit::approvedEntryDisplayed, m_leds->ledFuzzy, &KLed::off); connect(m_targetTextEdit, &TranslationUnitTextEdit::untranslatedEntryDisplayed, m_leds->ledUntr, &KLed::on); connect(m_targetTextEdit, &TranslationUnitTextEdit::translatedEntryDisplayed, m_leds->ledUntr, &KLed::off); m_targetTextEdit->showPos(m_targetTextEdit->currentPos()); } } void EditorView::gotoEntry() { return gotoEntry(DocPosition(), 0); } //main function in this file :) void EditorView::gotoEntry(DocPosition pos, int selection) { setUpdatesEnabled(false); bool refresh = (pos.entry == -1); if (refresh) pos = m_targetTextEdit->currentPos(); //qCWarning(LOKALIZE_LOG)<<"refresh"<isPlural(pos.entry))) { if (Q_UNLIKELY(m_catalog->numberOfPluralForms() != m_pluralTabBar->count())) { int i = m_pluralTabBar->count(); if (m_catalog->numberOfPluralForms() > m_pluralTabBar->count()) while (i < m_catalog->numberOfPluralForms()) m_pluralTabBar->addTab(i18nc("@title:tab", "Plural Form %1", ++i)); else while (i > m_catalog->numberOfPluralForms()) m_pluralTabBar->removeTab(i--); } m_pluralTabBar->show(); m_pluralTabBar->blockSignals(true); m_pluralTabBar->setCurrentIndex(pos.form); m_pluralTabBar->blockSignals(false); } else m_pluralTabBar->hide(); //bool keepCursor=DocPos(pos)==DocPos(_msgidEdit->currentPos()); bool keepCursor = false; CatalogString sourceWithTags = m_sourceTextEdit->showPos(pos, CatalogString(), keepCursor); //qCWarning(LOKALIZE_LOG)<<"calling showPos"; QString targetString = m_targetTextEdit->showPos(pos, sourceWithTags, keepCursor).string; //qCWarning(LOKALIZE_LOG)<<"ss"<<_msgstrEdit->textCursor().anchor()<<_msgstrEdit->textCursor().position(); m_sourceTextEdit->cursorToStart(); m_targetTextEdit->cursorToStart(); bool untrans = targetString.isEmpty(); //qCWarning(LOKALIZE_LOG)<<"ss1"<<_msgstrEdit->textCursor().anchor()<<_msgstrEdit->textCursor().position(); if (pos.offset || selection) { TranslationUnitTextEdit* msgEdit = (pos.part == DocPosition::Source ? m_sourceTextEdit : m_targetTextEdit); QTextCursor t = msgEdit->textCursor(); t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, pos.offset); //NOTE this was kinda bug due to on-the-fly msgid wordwrap if (selection) t.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selection); msgEdit->setTextCursor(t); } else if (!untrans) { QTextCursor t = m_targetTextEdit->textCursor(); //what if msg starts with a tag? if (Q_UNLIKELY(targetString.startsWith('<'))) { int offset = targetString.indexOf(QRegExp(QStringLiteral(">[^<]"))); if (offset != -1) t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, offset + 1); } else if (Q_UNLIKELY(targetString.startsWith(TAGRANGE_IMAGE_SYMBOL))) { int offset = targetString.indexOf(QRegExp(QStringLiteral("[^") % QChar(TAGRANGE_IMAGE_SYMBOL) % ']')); if (offset != -1) t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, offset + 1); } m_targetTextEdit->setTextCursor(t); } //qCWarning(LOKALIZE_LOG)<<"set-->"<<_msgstrEdit->textCursor().anchor()<<_msgstrEdit->textCursor().position(); //qCWarning(LOKALIZE_LOG)<<"anchor"<setFocus(); setUpdatesEnabled(true); } //BEGIN edit actions that are easier to do in this class void EditorView::unwrap() { unwrap(0); } void EditorView::unwrap(TranslationUnitTextEdit* editor) { if (!editor) editor = m_targetTextEdit; QTextCursor t = editor->document()->find(QRegExp("[^(\\\\n)]$")); if (t.isNull()) return; if (editor == m_targetTextEdit) m_catalog->beginMacro(i18nc("@item Undo action item", "Unwrap")); t.movePosition(QTextCursor::EndOfLine); if (!t.atEnd()) t.deleteChar(); QRegExp rx("[^(\\\\n)>]$"); //remove '\n's skipping "\\\\n" while (!(t = editor->document()->find(rx, t)).isNull()) { t.movePosition(QTextCursor::EndOfLine); if (!t.atEnd()) t.deleteChar(); } if (editor == m_targetTextEdit) m_catalog->endMacro(); } void EditorView::insertTerm(const QString& term) { m_targetTextEdit->insertPlainText(term); m_targetTextEdit->setFocus(); } QString EditorView::selectionInTarget() const { //TODO remove IMAGES return m_targetTextEdit->textCursor().selectedText(); } QString EditorView::selectionInSource() const { //TODO remove IMAGES return m_sourceTextEdit->textCursor().selectedText(); } void EditorView::setProperFocus() { m_targetTextEdit->setFocus(); } //END edit actions that are easier to do in this class QObject* EditorView::viewPort() { return m_targetTextEdit; } void EditorView::toggleBookmark(bool checked) { if (Q_UNLIKELY(m_targetTextEdit->currentPos().entry == -1)) return; m_catalog->setBookmark(m_targetTextEdit->currentPos().entry, checked); } void EditorView::toggleApprovement() { //qCWarning(LOKALIZE_LOG)<<"called"; if (Q_UNLIKELY(m_targetTextEdit->currentPos().entry == -1)) return; bool newState = !m_catalog->isApproved(m_targetTextEdit->currentPos().entry); SetStateCmd::push(m_catalog, m_targetTextEdit->currentPos(), newState); emit signalApprovedEntryDisplayed(newState); } void EditorView::setState(TargetState state) { if (Q_UNLIKELY(m_targetTextEdit->currentPos().entry == -1 || m_catalog->state(m_targetTextEdit->currentPos()) == state)) return; SetStateCmd::instantiateAndPush(m_catalog, m_targetTextEdit->currentPos(), state); emit signalApprovedEntryDisplayed(m_catalog->isApproved(m_targetTextEdit->currentPos())); } void EditorView::setEquivTrans(bool equivTrans) { m_catalog->push(new SetEquivTransCmd(m_catalog, m_targetTextEdit->currentPos(), equivTrans)); } diff --git a/src/editorview.h b/src/editorview.h index 2336f45..0a4dc02 100644 --- a/src/editorview.h +++ b/src/editorview.h @@ -1,140 +1,141 @@ /* **************************************************************************** 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 EDITORVIEW_H #define EDITORVIEW_H #include "pos.h" #include "state.h" #include "catalogstring.h" #include class Catalog; class LedsWidget; class TranslationUnitTextEdit; class QTabBar; class QContextMenuEvent; class QDragEnterEvent; /** * This is the main view class for Lokalize Editor. * Most of the non-menu, non-toolbar, non-statusbar, * and non-dockview editing GUI code should go here. * * There are several ways (for views) to modify current msg: * -modify KTextEdit and changes will be applied to catalog automatically (plus you need to care of fuzzy indication etc) * -modify catalog directly, then call EditorWindow::gotoEntry slot * I used both :) * * @short Main editor view: source and target textedits * @author Nick Shaforostoff */ class EditorView: public QSplitter { Q_OBJECT public: explicit EditorView(QWidget *, Catalog*); virtual ~EditorView(); QTabBar* tabBar() { return m_pluralTabBar; //to connect tabbar signals to controller (EditorWindow) slots } QString selectionInTarget() const;//for non-batch replace QString selectionInSource() const; QObject* viewPort(); void setProperFocus(); public slots: void gotoEntry(DocPosition pos, int selection/*, bool updateHistory=true*/); void gotoEntry(); void toggleApprovement(); void setState(TargetState); void setEquivTrans(bool); void settingsChanged(); void insertTerm(const QString&); //workaround for qt ctrl+z bug //Edit menu void unwrap(); void unwrap(TranslationUnitTextEdit* editor); /* void dragEnterEvent(QDragEnterEvent* event); void dropEvent(QDropEvent*); */ private: Catalog* m_catalog; TranslationUnitTextEdit * m_sourceTextEdit; TranslationUnitTextEdit * m_targetTextEdit ; QTabBar* m_pluralTabBar; LedsWidget* m_leds; public: bool m_modifiedAfterFind;//for F3-search reset signals: void signalEquivTranslatedEntryDisplayed(bool); void signalApprovedEntryDisplayed(bool); void signalChangeStatusbar(const QString&); void signalChanged(uint index); //esp for mergemode... void binaryUnitSelectRequested(const QString& id); void gotoEntryRequested(const DocPosition&); void tmLookupRequested(DocPosition::Part, const QString&); //void tmLookupRequested(const QString& source, const QString& target); void findRequested(); void findNextRequested(); void replaceRequested(); void doExplicitCompletion(); private slots: void resetFindForCurrent(const DocPosition& pos); void toggleBookmark(bool); }; class KLed; class QLabel; class LedsWidget: public QWidget { Q_OBJECT public: explicit LedsWidget(QWidget* parent); private: void contextMenuEvent(QContextMenuEvent* event) override; public slots: void cursorPositionChanged(int column); public: KLed* ledFuzzy; KLed* ledUntr; QLabel* lblColumn; }; #endif diff --git a/src/filesearch/filesearchtab.cpp b/src/filesearch/filesearchtab.cpp index f123ee2..209ec8b 100644 --- a/src/filesearch/filesearchtab.cpp +++ b/src/filesearch/filesearchtab.cpp @@ -1,921 +1,922 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "filesearchtab.h" #include "lokalize_debug.h" #include "ui_filesearchoptions.h" #include "ui_massreplaceoptions.h" #include "project.h" #include "prefs.h" #include "tmscanapi.h" //TODO separate some decls into new header #include "state.h" #include "qaview.h" #include "catalog.h" #include "fastsizehintitemdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QStringList doScanRecursive(const QDir& dir); class FileListModel: public QStringListModel { public: FileListModel(QObject* parent): QStringListModel(parent) {} QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex&) const override { return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } }; QVariant FileListModel::data(const QModelIndex& item, int role) const { if (role == Qt::DisplayRole) return shorterFilePath(stringList().at(item.row())); if (role == Qt::UserRole) return stringList().at(item.row()); return QVariant(); } SearchFileListView::SearchFileListView(QWidget* parent) : QDockWidget(i18nc("@title:window", "File List"), parent) , m_browser(new QTreeView(this)) , m_background(new QLabel(i18n("Drop translation files here..."), this)) , m_model(new FileListModel(this)) { setWidget(m_background); m_background->setMinimumWidth(QFontMetrics(font()).averageCharWidth() * 30); m_background->setAlignment(Qt::AlignCenter); m_browser->hide(); m_browser->setModel(m_model); m_browser->setRootIsDecorated(false); m_browser->setHeaderHidden(true); m_browser->setUniformRowHeights(true); m_browser->setAlternatingRowColors(true); m_browser->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* action = new QAction(i18nc("@action:inmenu", "Clear"), m_browser); connect(action, &QAction::triggered, this, &SearchFileListView::clear); m_browser->addAction(action); connect(m_browser, &QTreeView::activated, this, &SearchFileListView::requestFileOpen); } void SearchFileListView::requestFileOpen(const QModelIndex& item) { emit fileOpenRequested(item.data(Qt::UserRole).toString(), true); } void SearchFileListView::addFiles(const QStringList& files) { if (files.isEmpty()) return; m_background->hide(); setWidget(m_browser); m_browser->show(); //ensure unquiness, sorting the list along the way QMap map; foreach (const QString& filepath, m_model->stringList()) map[filepath] = true; foreach (const QString& filepath, files) map[filepath] = true; m_model->setStringList(map.keys()); } void SearchFileListView::addFilesFast(const QStringList& files) { if (files.size()) m_model->setStringList(m_model->stringList() + files); } void SearchFileListView::clear() { m_model->setStringList(QStringList()); } QStringList SearchFileListView::files() const { return m_model->stringList(); } void SearchFileListView::scrollTo(const QString& file) { if (file.isEmpty()) { m_browser->scrollToTop(); return; } int idx = m_model->stringList().indexOf(file); if (idx != -1) m_browser->scrollTo(m_model->index(idx, 0), QAbstractItemView::PositionAtCenter); } bool SearchParams::isEmpty() const { return sourcePattern.pattern().isEmpty() && targetPattern.pattern().isEmpty(); } SearchJob::SearchJob(const QStringList& f, const SearchParams& sp, const QVector& r, int sn, QObject*) : QRunnable() , files(f) , searchParams(sp) , rules(r) , searchNumber(sn) , m_size(0) { setAutoDelete(false); } void SearchJob::run() { QTime a; a.start(); bool removeAmpFromSource = searchParams.sourcePattern.patternSyntax() == QRegExp::FixedString && !searchParams.sourcePattern.pattern().contains(QLatin1Char('&')); bool removeAmpFromTarget = searchParams.targetPattern.patternSyntax() == QRegExp::FixedString && !searchParams.targetPattern.pattern().contains(QLatin1Char('&')); foreach (const QString& filePath, files) { Catalog catalog(0); if (Q_UNLIKELY(catalog.loadFromUrl(filePath, QString(), &m_size, true) != 0)) continue; //QVector catalogResults; int numberOfEntries = catalog.numberOfEntries(); DocPosition pos(0); for (; pos.entry < numberOfEntries; pos.entry++) { //if (!searchParams.states[catalog.state(pos)]) // return false; int lim = catalog.isPlural(pos.entry) ? catalog.numberOfPluralForms() : 1; for (pos.form = 0; pos.form < lim; pos.form++) { int sp = 0; int tp = 0; if (!searchParams.sourcePattern.isEmpty()) sp = searchParams.sourcePattern.indexIn(removeAmpFromSource ? catalog.source(pos).remove(QLatin1Char('&')) : catalog.source(pos)); if (!searchParams.targetPattern.isEmpty()) tp = searchParams.targetPattern.indexIn(removeAmpFromTarget ? catalog.target(pos).remove(QLatin1Char('&')) : catalog.target(pos)); //int np=searchParams.notesPattern.indexIn(catalog.notes(pos)); if ((sp != -1) != searchParams.invertSource && (tp != -1) != searchParams.invertTarget) { //TODO handle multiple results in same column //FileSearchResult r; SearchResult r; r.filepath = filePath; r.docPos = pos; if (!searchParams.sourcePattern.isEmpty() && !searchParams.invertSource) r.sourcePositions << StartLen(searchParams.sourcePattern.pos(), searchParams.sourcePattern.matchedLength()); if (!searchParams.targetPattern.isEmpty() && !searchParams.invertTarget) r.targetPositions << StartLen(searchParams.targetPattern.pos(), searchParams.targetPattern.matchedLength()); r.source = catalog.source(pos); r.target = catalog.target(pos); r.state = catalog.state(pos); r.isApproved = catalog.isApproved(pos); //r.activePhase=catalog.activePhase(); if (rules.size()) { QVector positions(2); int matchedQaRule = findMatchingRule(rules, r.source, r.target, positions); if (matchedQaRule == -1) continue; if (positions.at(0).len) r.sourcePositions << positions.at(0); if (positions.at(1).len) r.targetPositions << positions.at(1); } r.sourcePositions.squeeze(); r.targetPositions.squeeze(); //catalogResults< map; for (int i = 0; i < searchResults.count(); ++i) map.insertMulti(searchResults.at(i).filepath, i); foreach (const QString& filepath, map.keys()) { Catalog catalog(QThread::currentThread()); if (catalog.loadFromUrl(filepath, QString()) != 0) continue; foreach (int index, map.values(filepath)) { SearchResult& sr = searchResults[index]; DocPosition docPos = sr.docPos.toDocPosition(); if (catalog.target(docPos) != sr.target) { qCWarning(LOKALIZE_LOG) << "skipping replace because" << catalog.target(docPos) << "!=" << sr.target; continue; } CatalogString s = catalog.targetWithTags(docPos); int pos = replaceWhat.indexIn(s.string); while (pos != -1) { if (!s.string.midRef(pos, replaceWhat.matchedLength()).contains(TAGRANGE_IMAGE_SYMBOL)) { docPos.offset = pos; catalog.targetDelete(docPos, replaceWhat.matchedLength()); catalog.targetInsert(docPos, replaceWith); s.string.replace(pos, replaceWhat.matchedLength(), replaceWith); pos += replaceWith.length(); } else { pos += replaceWhat.matchedLength(); qCWarning(LOKALIZE_LOG) << "skipping replace because matched text contains markup" << s.string; } if (pos > s.string.length() || replaceWhat.pattern().startsWith('^')) break; pos = replaceWhat.indexIn(s.string, pos); } } catalog.save(); } } //BEGIN FileSearchModel FileSearchModel::FileSearchModel(QObject* parent) : QAbstractListModel(parent) { } QVariant FileSearchModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case FileSearchModel::Source: return i18nc("@title:column Original text", "Source"); case FileSearchModel::Target: return i18nc("@title:column Text in target language", "Target"); //case FileSearchModel::Context: return i18nc("@title:column","Context"); case FileSearchModel::Filepath: return i18nc("@title:column", "File"); case FileSearchModel::TranslationStatus: return i18nc("@title:column", "Translation Status"); } return QVariant(); } void FileSearchModel::appendSearchResults(const SearchResults& results) { beginInsertRows(QModelIndex(), m_searchResults.size(), m_searchResults.size() + results.size() - 1); m_searchResults += results; endInsertRows(); } void FileSearchModel::clear() { beginResetModel(); m_searchResults.clear();; endResetModel(); } QVariant FileSearchModel::data(const QModelIndex& item, int role) const { bool doHtml = (role == FastSizeHintItemDelegate::HtmlDisplayRole); if (doHtml) role = Qt::DisplayRole; if (role == Qt::DisplayRole) { QString result; const SearchResult& sr = m_searchResults.at(item.row()); if (item.column() == Source) result = sr.source; if (item.column() == Target) result = sr.target; if (item.column() == Filepath) result = shorterFilePath(sr.filepath); if (doHtml && item.column() <= FileSearchModel::Target) { if (result.isEmpty()) return result; const QString startBld = QStringLiteral("_ST_"); const QString endBld = QStringLiteral("_END_"); const QString startBldTag = QStringLiteral(""); const QString endBldTag = QStringLiteral(""); if (item.column() == FileSearchModel::Target && !m_replaceWhat.isEmpty()) { result.replace(m_replaceWhat, m_replaceWith); QString escaped = convertToHtml(result, !sr.isApproved); escaped.replace(startBld, startBldTag); escaped.replace(endBld, endBldTag); return escaped; } const QVector& occurrences = item.column() == FileSearchModel::Source ? sr.sourcePositions : sr.targetPositions; int occ = occurrences.count(); while (--occ >= 0) { const StartLen& sl = occurrences.at(occ); result.insert(sl.start + sl.len, endBld); result.insert(sl.start, startBld); } /* !isApproved(sr.state, Project::instance()->local()->role())*/ QString escaped = convertToHtml(result, item.column() == FileSearchModel::Target && !sr.isApproved); escaped.replace(startBld, startBldTag); escaped.replace(endBld, endBldTag); return escaped; } return result; } if (role == Qt::UserRole) { const SearchResult& sr = m_searchResults.at(item.row()); if (item.column() == Filepath) return sr.filepath; } return QVariant(); } void FileSearchModel::setReplacePreview(const QRegExp& s, const QString& r) { m_replaceWhat = s; m_replaceWith = QLatin1String("_ST_") % r % QLatin1String("_END_"); emit dataChanged(index(0, Target), index(rowCount() - 1, Target)); } //END FileSearchModel //BEGIN FileSearchTab FileSearchTab::FileSearchTab(QWidget *parent) : LokalizeSubwindowBase2(parent) // , m_proxyModel(new TMResultsSortFilterProxyModel(this)) , m_model(new FileSearchModel(this)) , m_lastSearchNumber(0) , m_dbusId(-1) { setWindowTitle(i18nc("@title:window", "Search and replace in files")); setAcceptDrops(true); QWidget* w = new QWidget(this); ui_fileSearchOptions = new Ui_FileSearchOptions; ui_fileSearchOptions->setupUi(w); setCentralWidget(w); QShortcut* sh = new QShortcut(Qt::CTRL + Qt::Key_L, this); connect(sh, &QShortcut::activated, ui_fileSearchOptions->querySource, QOverload<>::of(&QLineEdit::setFocus)); setFocusProxy(ui_fileSearchOptions->querySource); sh = new QShortcut(Qt::Key_Escape, this, SLOT(stopSearch()), 0, Qt::WidgetWithChildrenShortcut); QTreeView* view = ui_fileSearchOptions->treeView; QVector singleLineColumns(FileSearchModel::ColumnCount, false); singleLineColumns[FileSearchModel::Filepath] = true; singleLineColumns[FileSearchModel::TranslationStatus] = true; //singleLineColumns[TMDBModel::Context]=true; QVector richTextColumns(FileSearchModel::ColumnCount, false); richTextColumns[FileSearchModel::Source] = true; richTextColumns[FileSearchModel::Target] = true; view->setItemDelegate(new FastSizeHintItemDelegate(this, singleLineColumns, richTextColumns)); connect(m_model, &FileSearchModel::modelReset, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_model, &FileSearchModel::dataChanged, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); //connect(m_model,SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),view->itemDelegate(),SLOT(reset())); //connect(m_proxyModel,SIGNAL(layoutChanged()),view->itemDelegate(),SLOT(reset())); //connect(m_proxyModel,SIGNAL(layoutChanged()),this,SLOT(displayTotalResultCount())); view->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* a = new QAction(i18n("Copy source to clipboard"), view); a->setShortcut(Qt::CTRL + Qt::Key_S); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::copySourceToClipboard); view->addAction(a); a = new QAction(i18n("Copy target to clipboard"), view); a->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::copyTargetToClipboard); view->addAction(a); a = new QAction(i18n("Open file"), view); a->setShortcut(QKeySequence(Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &FileSearchTab::openFile); connect(view, &QTreeView::activated, this, &FileSearchTab::openFile); view->addAction(a); connect(ui_fileSearchOptions->querySource, &QLineEdit::returnPressed, this, &FileSearchTab::performSearch); connect(ui_fileSearchOptions->queryTarget, &QLineEdit::returnPressed, this, &FileSearchTab::performSearch); connect(ui_fileSearchOptions->doFind, &QPushButton::clicked, this, &FileSearchTab::performSearch); // m_proxyModel->setDynamicSortFilter(true); // m_proxyModel->setSourceModel(m_model); view->setModel(m_model); // view->setModel(m_proxyModel); // view->sortByColumn(FileSearchModel::Filepath,Qt::AscendingOrder); // view->setSortingEnabled(true); // view->setItemDelegate(new FastSizeHintItemDelegate(this)); // connect(m_model,SIGNAL(resultsFetched()),view->itemDelegate(),SLOT(reset())); // connect(m_model,SIGNAL(modelReset()),view->itemDelegate(),SLOT(reset())); // connect(m_proxyModel,SIGNAL(layoutChanged()),view->itemDelegate(),SLOT(reset())); // connect(m_proxyModel,SIGNAL(layoutChanged()),this,SLOT(displayTotalResultCount())); //BEGIN resizeColumnToContents static const int maxInitialWidths[] = {QApplication::desktop()->availableGeometry().width() / 3, QApplication::desktop()->availableGeometry().width() / 3}; int column = sizeof(maxInitialWidths) / sizeof(int); while (--column >= 0) view->setColumnWidth(column, maxInitialWidths[column]); //END resizeColumnToContents int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); setXMLFile(QStringLiteral("filesearchtabui.rc"), true); dbusObjectPath(); KActionCollection* ac = actionCollection(); KActionCategory* srf = new KActionCategory(i18nc("@title actions category", "Search and replace in files"), ac); m_searchFileListView = new SearchFileListView(this); //m_searchFileListView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_searchFileListView); srf->addAction(QStringLiteral("showfilelist_action"), m_searchFileListView->toggleViewAction()); connect(m_searchFileListView, &SearchFileListView::fileOpenRequested, this, QOverload::of(&FileSearchTab::fileOpenRequested)); m_massReplaceView = new MassReplaceView(this); addDockWidget(Qt::RightDockWidgetArea, m_massReplaceView); srf->addAction(QStringLiteral("showmassreplace_action"), m_massReplaceView->toggleViewAction()); connect(m_massReplaceView, &MassReplaceView::previewRequested, m_model, &FileSearchModel::setReplacePreview); connect(m_massReplaceView, &MassReplaceView::replaceRequested, this, &FileSearchTab::massReplace); //m_massReplaceView->hide(); m_qaView = new QaView(this); m_qaView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_qaView); srf->addAction(QStringLiteral("showqa_action"), m_qaView->toggleViewAction()); connect(m_qaView, &QaView::rulesChanged, this, &FileSearchTab::performSearch); connect(m_qaView->toggleViewAction(), &QAction::toggled, this, &FileSearchTab::performSearch, Qt::QueuedConnection); view->header()->restoreState(readUiState("FileSearchResultsHeaderState")); } FileSearchTab::~FileSearchTab() { stopSearch(); writeUiState("FileSearchResultsHeaderState", ui_fileSearchOptions->treeView->header()->saveState()); ids.removeAll(m_dbusId); } void FileSearchTab::performSearch() { if (m_searchFileListView->files().isEmpty()) { addFilesToSearch(doScanRecursive(QDir(Project::instance()->poDir()))); if (m_searchFileListView->files().isEmpty()) return; } m_model->clear(); statusBarItems.insert(1, QString()); m_searchFileListView->scrollTo(); m_lastSearchNumber++; SearchParams sp; sp.sourcePattern.setPattern(ui_fileSearchOptions->querySource->text()); sp.targetPattern.setPattern(ui_fileSearchOptions->queryTarget->text()); sp.invertSource = ui_fileSearchOptions->invertSource->isChecked(); sp.invertTarget = ui_fileSearchOptions->invertTarget->isChecked(); QVector rules = m_qaView->isVisible() ? m_qaView->rules() : QVector(); if (sp.isEmpty() && rules.isEmpty()) return; if (!ui_fileSearchOptions->regEx->isChecked()) { sp.sourcePattern.setPatternSyntax(QRegExp::FixedString); sp.targetPattern.setPatternSyntax(QRegExp::FixedString); } /* else { sp.sourcePattern.setMinimal(true); sp.targetPattern.setMinimal(true); } */ stopSearch(); m_massReplaceView->deactivatePreview(); QStringList files = m_searchFileListView->files(); for (int i = 0; i < files.size(); i += 100) { QStringList batch; int lim = qMin(files.size(), i + 100); for (int j = i; j < lim; j++) batch.append(files.at(j)); SearchJob* job = new SearchJob(batch, sp, rules, m_lastSearchNumber); QObject::connect(job, &SearchJob::done, this, &FileSearchTab::searchJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } void FileSearchTab::stopSearch() { #if QT_VERSION >= 0x050500 int i = m_runningJobs.size(); while (--i >= 0) QThreadPool::globalInstance()->cancel(m_runningJobs.at(i)); #endif m_runningJobs.clear(); } void FileSearchTab::massReplace(const QRegExp &what, const QString& with) { #define BATCH_SIZE 20 SearchResults searchResults = m_model->searchResults(); for (int i = 0; i < searchResults.count(); i += BATCH_SIZE) { int last = qMin(i + BATCH_SIZE, searchResults.count() - 1); QString filepath = searchResults.at(last).filepath; while (last + 1 < searchResults.count() && filepath == searchResults.at(last + 1).filepath) ++last; MassReplaceJob* job = new MassReplaceJob(searchResults.mid(i, last + 1 - i), i, what, with); QObject::connect(job, &MassReplaceJob::done, this, &FileSearchTab::replaceJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } static void copy(QTreeView* view, int column) { QApplication::clipboard()->setText(view->currentIndex().sibling(view->currentIndex().row(), column).data().toString()); } void FileSearchTab::copySourceToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Source); } void FileSearchTab::copyTargetToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Target); } void FileSearchTab::openFile() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); SearchResult sr = m_model->searchResult(item); DocPosition docPos = sr.docPos.toDocPosition(); int selection = 0; if (sr.targetPositions.size()) { docPos.offset = sr.targetPositions.first().start; selection = sr.targetPositions.first().len; } qCDebug(LOKALIZE_LOG) << "fileOpenRequest" << docPos.offset << selection; emit fileOpenRequested(sr.filepath, docPos, selection, true); } void FileSearchTab::fileSearchNext() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); int row = item.row(); int rowCount = m_model->rowCount(); if (++row >= rowCount) //ok if row was -1 (no solection) return; ui_fileSearchOptions->treeView->setCurrentIndex(item.sibling(row, item.column())); openFile(); } QStringList scanRecursive(const QList& urls) { QStringList result; int i = urls.size(); while (--i >= 0) { if (urls.at(i).isEmpty() || urls.at(i).path().isEmpty()) //NOTE is this a Qt bug? continue; QString path = urls.at(i).toLocalFile(); if (Catalog::extIsSupported(path)) result.append(path); else result += doScanRecursive(QDir(path)); } return result; } //returns gross number of jobs started static QStringList doScanRecursive(const QDir& dir) { QStringList result; QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) result += doScanRecursive(QDir(dir.filePath(subDirs.at(i)))); QStringList filters = Catalog::supportedExtensions(); i = filters.size(); while (--i >= 0) filters[i].prepend('*'); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); while (--i >= 0) result.append(dir.filePath(files.at(i))); return result; } void FileSearchTab::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void FileSearchTab::dropEvent(QDropEvent *event) { event->acceptProposedAction(); addFilesToSearch(scanRecursive(event->mimeData()->urls())); } void FileSearchTab::addFilesToSearch(const QStringList& files) { m_searchFileListView->addFiles(files); performSearch(); } void FileSearchTab::setSourceQuery(const QString& query) { ui_fileSearchOptions->querySource->setText(query); } void FileSearchTab::setTargetQuery(const QString& query) { ui_fileSearchOptions->queryTarget->setText(query); } void FileSearchTab::searchJobDone(SearchJob* j) { j->deleteLater(); if (j->searchNumber != m_lastSearchNumber) return; /* SearchResults searchResults; FileSearchResults::const_iterator i = j->results.constBegin(); while (i != j->results.constEnd()) { foreach(const FileSearchResult& fsr, i.value()) { SearchResult sr(fsr); sr.filepath=i.key(); searchResults<appendSearchResults(searchResults); */ if (j->results.size()) { m_model->appendSearchResults(j->results); m_searchFileListView->scrollTo(j->results.last().filepath); } statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1", m_model->rowCount())); //ui_fileSearchOptions->treeView->setFocus(); } void FileSearchTab::replaceJobDone(MassReplaceJob* j) { j->deleteLater(); ui_fileSearchOptions->treeView->scrollTo(m_model->index(j->globalPos + j->searchResults.count(), 0)); } //END FileSearchTab //BEGIN MASS REPLACE MassReplaceView::MassReplaceView(QWidget* parent) : QDockWidget(i18nc("@title:window", "Mass replace"), parent) , ui(new Ui_MassReplaceOptions) { QWidget* base = new QWidget(this); setWidget(base); ui->setupUi(base); connect(ui->doPreview, &QPushButton::toggled, this, &MassReplaceView::requestPreview); connect(ui->doReplace, &QPushButton::clicked, this, &MassReplaceView::requestReplace); /* QLabel* rl=new QLabel(i18n("Replace:"), base); QLineEdit* searchEdit=new QLineEdit(base); QHBoxLayout* searchL=new QHBoxLayout(); searchL->addWidget(rl); searchL->addWidget(searchEdit); QLabel* wl=new QLabel(i18n("With:"), base); wl->setAlignment(Qt::AlignRight); wl->setMinimumSize(rl->minimumSizeHint()); QLineEdit* replacementEdit=new QLineEdit(base); QHBoxLayout* replacementL=new QHBoxLayout(); replacementL->addWidget(wl); replacementL->addWidget(replacementEdit); FlowLayout* fl=new FlowLayout(); fl->addItem(searchL); fl->addItem(replacementL); base->setLayout(fl); */ } MassReplaceView::~MassReplaceView() { delete ui; } static QRegExp regExpFromUi(const QString& s, Ui_MassReplaceOptions* ui) { return QRegExp(s, ui->matchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive, ui->useRegExps->isChecked() ? QRegExp::RegExp : QRegExp::FixedString); } void MassReplaceView::requestPreviewUpdate() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.length()) ui->doReplace->setEnabled(true); emit previewRequested(regExpFromUi(s, ui), r); } void MassReplaceView::requestPreview(bool enable) { if (enable) { connect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); connect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); requestPreviewUpdate(); } else { disconnect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); emit previewRequested(QRegExp(), QString()); } } void MassReplaceView::requestReplace() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.isEmpty()) return; emit replaceRequested(regExpFromUi(s, ui), r); } void MassReplaceView::deactivatePreview() { ui->doPreview->setChecked(false); ui->doReplace->setEnabled(false); } #include "filesearchadaptor.h" #include QList FileSearchTab::ids; //BEGIN DBus interface QString FileSearchTab::dbusObjectPath() { QString FILESEARCH_PATH = QStringLiteral("/ThisIsWhatYouWant/FileSearch/"); if (m_dbusId == -1) { new FileSearchAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(FILESEARCH_PATH + QString::number(m_dbusId), this); } return FILESEARCH_PATH + QString::number(m_dbusId); } bool FileSearchTab::findGuiTextPackage(QString text, QString package) { setSourceQuery(text); performSearch(); return true; } //END DBus interface diff --git a/src/filesearch/filesearchtab.h b/src/filesearch/filesearchtab.h index bf816e7..fcc8d2a 100644 --- a/src/filesearch/filesearchtab.h +++ b/src/filesearch/filesearchtab.h @@ -1,333 +1,334 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2012 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 FILESEARCHTAB_H #define FILESEARCHTAB_H #include "lokalizesubwindowbase.h" #include "pos.h" #include "rule.h" #include #include #include #include class MassReplaceJob; class SearchJob; class QRunnable; class QLabel; class QaView; class QStringListModel; class QComboBox; class QTreeView; class QSortFilterProxyModel; class KXMLGUIClient; class FileSearchModel; class SearchFileListView; class MassReplaceView; class Ui_FileSearchOptions; /** * Global file search/repalce tab */ class FileSearchTab: public LokalizeSubwindowBase2 { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.FileSearch") //qdbuscpp2xml -m -s filesearch/filesearchtab.h -o filesearch/org.kde.lokalize.FileSearch.xml public: explicit FileSearchTab(QWidget *parent); ~FileSearchTab() override; void hideDocks() override {} void showDocks() override {} KXMLGUIClient* guiClient() override { return (KXMLGUIClient*)this; } QString dbusObjectPath(); int dbusId() { return m_dbusId; } public slots: void copySourceToClipboard(); void copyTargetToClipboard(); void openFile(); Q_SCRIPTABLE void performSearch(); Q_SCRIPTABLE void addFilesToSearch(const QStringList&); Q_SCRIPTABLE void setSourceQuery(const QString&); Q_SCRIPTABLE void setTargetQuery(const QString&); Q_SCRIPTABLE bool findGuiText(QString text) { return findGuiTextPackage(text, QString()); } Q_SCRIPTABLE bool findGuiTextPackage(QString text, QString package); void fileSearchNext(); void stopSearch(); void massReplace(const QRegExp &what, const QString& with); private slots: void searchJobDone(SearchJob*); void replaceJobDone(MassReplaceJob*); signals: void fileOpenRequested(const QString& filePath, DocPosition docPos, int selection, const bool setAsActive); void fileOpenRequested(const QString& filePath, const bool setAsActive); private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent*) override; private: Ui_FileSearchOptions* ui_fileSearchOptions; FileSearchModel* m_model; //TMResultsSortFilterProxyModel *m_proxyModel; SearchFileListView* m_searchFileListView; MassReplaceView* m_massReplaceView; QaView* m_qaView; QVector m_runningJobs; //to avoid results from previous search showing up in the new one int m_lastSearchNumber; int m_dbusId; static QList ids; }; struct FileSearchResult { DocPos docPos; QString source; QString target; bool isApproved; TargetState state; //Phase activePhase; QVector sourcePositions; QVector targetPositions; //int matchedQaRule; //short notePos; //char noteindex; }; typedef QMap > FileSearchResults; struct SearchResult: public FileSearchResult { QString filepath; SearchResult(const FileSearchResult& fsr): FileSearchResult(fsr) {} SearchResult() {} }; typedef QVector SearchResults; class FileSearchModel: public QAbstractListModel { Q_OBJECT public: enum FileSearchModelColumns { Source = 0, Target, //Context, Filepath, TranslationStatus, //Notes, ColumnCount }; enum Roles { FullPathRole = Qt::UserRole, TransStateRole = Qt::UserRole + 1, HtmlDisplayRole = Qt::UserRole + 2 }; explicit FileSearchModel(QObject* parent); ~FileSearchModel() override {} QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent) return ColumnCount; } int rowCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent) return m_searchResults.size(); } SearchResults searchResults()const { return m_searchResults; } SearchResult searchResult(const QModelIndex& item) const { return m_searchResults.at(item.row()); } void appendSearchResults(const SearchResults&); void clear(); public slots: void setReplacePreview(const QRegExp&, const QString&); private: SearchResults m_searchResults; QRegExp m_replaceWhat; QString m_replaceWith; }; class SearchFileListView: public QDockWidget { Q_OBJECT public: explicit SearchFileListView(QWidget*); ~SearchFileListView() {} void addFiles(const QStringList& files); void addFilesFast(const QStringList& files); QStringList files()const; void scrollTo(const QString& file = QString()); public slots: void clear(); void requestFileOpen(const QModelIndex&); signals: void fileOpenRequested(const QString& filePath, const bool setAsActive); private: QTreeView* m_browser; QLabel* m_background; QStringListModel* m_model; }; class Ui_MassReplaceOptions; class MassReplaceView: public QDockWidget { Q_OBJECT public: explicit MassReplaceView(QWidget*); ~MassReplaceView(); void deactivatePreview(); signals: void previewRequested(const QRegExp&, const QString&); void replaceRequested(const QRegExp&, const QString&); private slots: void requestPreview(bool enable); void requestPreviewUpdate(); void requestReplace(); private: Ui_MassReplaceOptions* ui; }; struct SearchParams { QRegExp sourcePattern; QRegExp targetPattern; QRegExp notesPattern; bool invertSource; bool invertTarget; bool states[StateCount]; bool isEmpty() const; SearchParams(): invertSource(false), invertTarget(false) { memset(states, 0, sizeof(states)); } }; #include class SearchJob: public QObject, public QRunnable { Q_OBJECT public: explicit SearchJob(const QStringList& f, const SearchParams& sp, const QVector& r, int sn, QObject* parent = nullptr); ~SearchJob() override {} signals: void done(SearchJob*); protected: void run() override; public: QStringList files; SearchParams searchParams; QVector rules; int searchNumber; SearchResults results; //plain int m_size; }; /// @short replace in files class MassReplaceJob: public QObject, public QRunnable { Q_OBJECT public: explicit MassReplaceJob(const SearchResults& srs, int pos, const QRegExp& s, const QString& r, //int sn, QObject* parent = nullptr); ~MassReplaceJob() override {} signals: void done(MassReplaceJob*); protected: void run() override; public: SearchResults searchResults; int globalPos; QRegExp replaceWhat; QString replaceWith; }; #endif diff --git a/src/glossary/glossary.cpp b/src/glossary/glossary.cpp index 29a1cd5..67c9cbd 100644 --- a/src/glossary/glossary.cpp +++ b/src/glossary/glossary.cpp @@ -1,735 +1,736 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "glossary.h" #include "lokalize_debug.h" #include "stemming.h" // #include "tbxparser.h" #include "project.h" #include "prefs_lokalize.h" #include "domroutines.h" #include #include #include #include #include #include #include #include using namespace GlossaryNS; static const QString defaultLang = QStringLiteral("en_US"); static const QString xmlLang = QStringLiteral("xml:lang"); static const QString ntig = QStringLiteral("ntig"); static const QString tig = QStringLiteral("tig"); static const QString termGrp = QStringLiteral("termGrp"); static const QString langSet = QStringLiteral("langSet"); static const QString term = QStringLiteral("term"); static const QString id = QStringLiteral("id"); QList Glossary::idsForLangWord(const QString& lang, const QString& word) const { return idsByLangWord[lang].values(word); } Glossary::Glossary(QObject* parent) : QObject(parent) , m_clean(true) { } //BEGIN DISK bool Glossary::load(const QString& newPath) { QTime a; a.start(); //BEGIN NEW QIODevice* device = new QFile(newPath); if (!device->open(QFile::ReadOnly | QFile::Text)) { delete device; //return; device = new QBuffer(); static_cast(device)->setData(QByteArray( "\n" "\n" "\n" " \n" " \n" " \n" " \n" "\n" )); } QXmlSimpleReader reader; //reader.setFeature("http://qtsoftware.com/xml/features/report-whitespace-only-CharData",true); reader.setFeature("http://xml.org/sax/features/namespaces", false); QXmlInputSource source(device); QDomDocument newDoc; QString errorMsg; int errorLine;//+errorColumn; bool success = newDoc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); delete device; if (!success) { qCWarning(LOKALIZE_LOG) << errorMsg; return false; //errorLine+1; } clear();//does setClean(true); m_path = newPath; m_doc = newDoc; //QDomElement file=m_doc.elementsByTagName("file").at(0).toElement(); m_entries = m_doc.elementsByTagName(QStringLiteral("termEntry")); for (int i = 0; i < m_entries.size(); i++) hashTermEntry(m_entries.at(i).toElement()); m_idsForEntriesById = m_entriesById.keys(); //END NEW #if 0 TbxParser parser(this); QXmlSimpleReader reader1; reader1.setContentHandler(&parser); QFile file(p); if (!file.open(QFile::ReadOnly | QFile::Text)) return; QXmlInputSource xmlInputSource(&file); if (!reader1.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load " << path; #endif emit loaded(); if (a.elapsed() > 50) qCDebug(LOKALIZE_LOG) << "glossary loaded in" << a.elapsed(); return true; } bool Glossary::save() { if (m_path.isEmpty()) return false; QFile* device = new QFile(m_path); if (!device->open(QFile::WriteOnly | QFile::Truncate)) { device->deleteLater(); return false; } QTextStream stream(device); m_doc.save(stream, 2); device->deleteLater(); setClean(true); return true; } void Glossary::setClean(bool clean) { m_clean = clean; emit changed();//may be emitted multiple times in a row. so what? :) } //END DISK //BEGIN MODEL #define FETCH_SIZE 128 void GlossarySortFilterProxyModel::setFilterRegExp(const QString& s) { if (!sourceModel()) return; //static const QRegExp lettersOnly("^[a-z]"); QSortFilterProxyModel::setFilterRegExp(s); fetchMore(QModelIndex()); } void GlossarySortFilterProxyModel::fetchMore(const QModelIndex&) { int expectedCount = rowCount() + FETCH_SIZE / 2; while (rowCount(QModelIndex()) < expectedCount && sourceModel()->canFetchMore(QModelIndex())) { sourceModel()->fetchMore(QModelIndex()); //qCDebug(LOKALIZE_LOG)<<"filter:"<rowCount(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); } } GlossaryModel::GlossaryModel(QObject* parent) : QAbstractListModel(parent) , m_visibleCount(0) , m_glossary(Project::instance()->glossary()) { connect(m_glossary, &Glossary::loaded, this, &GlossaryModel::forceReset); } void GlossaryModel::forceReset() { beginResetModel(); m_visibleCount = 0; endResetModel(); } bool GlossaryModel::canFetchMore(const QModelIndex&) const { return false;//!parent.isValid() && m_glossary->size()!=m_visibleCount; } void GlossaryModel::fetchMore(const QModelIndex& parent) { int newVisibleCount = qMin(m_visibleCount + FETCH_SIZE, m_glossary->size()); beginInsertRows(parent, m_visibleCount, newVisibleCount - 1); m_visibleCount = newVisibleCount; endInsertRows(); } int GlossaryModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_glossary->size();//m_visibleCount; } QVariant GlossaryModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { //case ID: return i18nc("@title:column","ID"); case English: return i18nc("@title:column Original text", "Source");; case Target: return i18nc("@title:column Text in target language", "Target"); case SubjectField: return i18nc("@title:column", "Subject Field"); } return QVariant(); } QVariant GlossaryModel::data(const QModelIndex& index, int role) const { //if (role==Qt::SizeHintRole) // return QVariant(QSize(50, 30)); if (role != Qt::DisplayRole) return QVariant(); static const QString nl = QStringLiteral(" ") + QChar(0x00B7) + ' '; static Project* project = Project::instance(); Glossary* glossary = m_glossary; QByteArray id = glossary->id(index.row()); switch (index.column()) { case ID: return id; case English: return glossary->terms(id, project->sourceLangCode()).join(nl); case Target: return glossary->terms(id, project->targetLangCode()).join(nl); case SubjectField: return glossary->subjectField(id); } return QVariant(); } /* QModelIndex GlossaryModel::index (int row,int column,const QModelIndex& parent) const { return createIndex (row, column); } */ int GlossaryModel::columnCount(const QModelIndex&) const { return GlossaryModelColumnCount; } /* Qt::ItemFlags GlossaryModel::flags ( const QModelIndex & index ) const { return Qt::ItemIsSelectable|Qt::ItemIsEnabled; //if (index.column()==FuzzyFlag) // return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; //return QAbstractItemModel::flags(index); } */ //END MODEL general (GlossaryModel continues below) //BEGIN EDITING QByteArray Glossary::generateNewId() { // generate unique ID int idNumber = 0; QList busyIdNumbers; QString authorId(Settings::authorName().toLower()); authorId.replace(' ', '_'); QRegExp rx('^' % authorId % QStringLiteral("\\-([0-9]*)$")); foreach (const QByteArray& id, m_idsForEntriesById) { if (rx.exactMatch(QString::fromLatin1(id))) busyIdNumbers.append(rx.cap(1).toInt()); } int i = removedIds.size(); while (--i >= 0) { if (rx.exactMatch(QString::fromLatin1(removedIds.at(i)))) busyIdNumbers.append(rx.cap(1).toInt()); } if (!busyIdNumbers.isEmpty()) { qSort(busyIdNumbers); while (busyIdNumbers.contains(idNumber)) ++idNumber; } return authorId.toLatin1() + '-' + QByteArray::number(idNumber); } QStringList Glossary::subjectFields() const { QSet result; foreach (const QByteArray& id, m_idsForEntriesById) result.insert(subjectField(id)); return result.toList(); } QByteArray Glossary::id(int index) const { if (index < m_idsForEntriesById.size()) return m_idsForEntriesById.at(index); return QByteArray(); } QStringList Glossary::terms(const QByteArray& id, const QString& language) const { QString minusLang = language; minusLang.replace('_', '-'); QStringRef soleLang = language.leftRef(2); QStringList result; QDomElement n = m_entriesById.value(id).firstChildElement(langSet); while (!n.isNull()) { QString lang = n.attribute(xmlLang, defaultLang); if (language == lang || minusLang == lang || soleLang == lang) { QDomElement ntigElem = n.firstChildElement(ntig); while (!ntigElem.isNull()) { result << ntigElem.firstChildElement(termGrp).firstChildElement(term).text(); ntigElem = ntigElem.nextSiblingElement(ntig); } QDomElement tigElem = n.firstChildElement(tig); while (!tigElem.isNull()) { result << tigElem.firstChildElement(term).text(); tigElem = tigElem.nextSiblingElement(tig); } } n = n.nextSiblingElement(langSet); } return result; } // QDomElement ourLangSetElement will reference the lang tag we want (if it exists) static void getElementsForTermLangIndex(QDomElement termEntry, QString& lang, int index, QDomElement& ourLangSetElement, QDomElement& tigElement, //<-- can point to as well QDomElement& termElement) { QString minusLang = lang; minusLang.replace('_', '-'); QStringRef soleLang = lang.leftRef(2); //qCDebug(LOKALIZE_LOG)<<"started walking over"<& wordHash, // const QString& what, // int index) void Glossary::hashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); if (entryId.isEmpty()) return; m_entriesById.insert(entryId, termEntry); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].insert(stem(sourceLangCode, word), entryId); } } void Glossary::unhashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); m_entriesById.remove(entryId); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].remove(stem(sourceLangCode, word), entryId); } } #if 0 void Glossary::hashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.insert(stem(Project::instance()->sourceLangCode(), word), index); } } void Glossary::unhashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.remove(stem(Project::instance()->sourceLangCode(), word), index); } } #endif void Glossary::removeEntry(const QByteArray& id) { if (!m_entriesById.contains(id)) return; QDomElement entry = m_entriesById.value(id); if (entry.nextSibling().isCharacterData()) entry.parentNode().removeChild(entry.nextSibling()); //nice formatting entry.parentNode().removeChild(entry); m_entriesById.remove(id); unhashTermEntry(entry); m_idsForEntriesById = m_entriesById.keys(); removedIds.append(id); //for new id generation goodness setClean(false); } static void appendTerm(QDomElement langSetElem, const QString& termText) { QDomDocument doc = langSetElem.ownerDocument(); /* QDomElement ntigElement=doc.createElement(ntig); langSetElem.appendChild(ntigElement); QDomElement termGrpElement=doc.createElement(termGrp); ntigElement.appendChild(termGrpElement); QDomElement termElement=doc.createElement(term); termGrpElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); */ QDomElement tigElement = doc.createElement(tig); langSetElem.appendChild(tigElement); QDomElement termElement = doc.createElement(term); tigElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); } QByteArray Glossary::append(const QStringList& sourceTerms, const QStringList& targetTerms) { if (!m_doc.elementsByTagName(QStringLiteral("body")).count()) return QByteArray(); setClean(false); QDomElement termEntry = m_doc.createElement(QStringLiteral("termEntry")); m_doc.elementsByTagName(QStringLiteral("body")).at(0).appendChild(termEntry); //m_entries=m_doc.elementsByTagName("termEntry"); QByteArray newId = generateNewId(); termEntry.setAttribute(::id, QString::fromLatin1(newId)); QDomElement sourceElem = m_doc.createElement(langSet); termEntry.appendChild(sourceElem); sourceElem.setAttribute(xmlLang, Project::instance()->sourceLangCode().replace('_', '-')); foreach (QString sourceTerm, sourceTerms) appendTerm(sourceElem, sourceTerm); QDomElement targetElem = m_doc.createElement(langSet); termEntry.appendChild(targetElem); targetElem.setAttribute(xmlLang, Project::instance()->targetLangCode().replace('_', '-')); foreach (QString targetTerm, targetTerms) appendTerm(targetElem, targetTerm); hashTermEntry(termEntry); m_idsForEntriesById = m_entriesById.keys(); return newId; } void Glossary::append(const QString& _english, const QString& _target) { append(QStringList(_english), QStringList(_target)); } void Glossary::clear() { setClean(true); //path.clear(); idsByLangWord.clear(); m_entriesById.clear(); m_idsForEntriesById.clear(); removedIds.clear(); changedIds_.clear(); addedIds_.clear(); wordHash_.clear(); termList_.clear(); langWordEntry_.clear(); subjectFields_ = QStringList(QString()); m_doc.clear(); } bool GlossaryModel::removeRows(int row, int count, const QModelIndex& parent) { beginRemoveRows(parent, row, row + count - 1); Glossary* glossary = Project::instance()->glossary(); int i = row + count; while (--i >= row) glossary->removeEntry(glossary->id(i)); endRemoveRows(); return true; } // bool GlossaryModel::insertRows(int row,int count,const QModelIndex& parent) // { // if (row!=rowCount()) // return false; QByteArray GlossaryModel::appendRow(const QString& _english, const QString& _target) { bool notify = !canFetchMore(QModelIndex()); if (notify) beginInsertRows(QModelIndex(), rowCount(), rowCount()); QByteArray id = m_glossary->append(QStringList(_english), QStringList(_target)); if (notify) { m_visibleCount++; endInsertRows(); } return id; } //END EDITING diff --git a/src/glossary/glossary.h b/src/glossary/glossary.h index c623323..e0cfdb1 100644 --- a/src/glossary/glossary.h +++ b/src/glossary/glossary.h @@ -1,251 +1,252 @@ /* **************************************************************************** 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 GLOSSARY_H #define GLOSSARY_H #include #include #include #include #include #include #include /** * Classes for TBX Glossary handling */ namespace GlossaryNS { /** * struct that contains types data we work with. * this data can also be added to the TBX file * * the entry represents term, not word(s), * so there can be only one subjectField. * * @short Contains parts of 'entry' tag in TBX that we support */ struct TermEntry { QStringList english; QStringList target; QString definition; int subjectField; //index in global Glossary's subjectFields list QString id; //used to identify entry on edit action //TODO TermEntry(const QStringList& _english, const QStringList& _target, const QString& _definition, int _subjectField, const QString& _id = QString() ) : english(_english) , target(_target) , definition(_definition) , subjectField(_subjectField) , id(_id) {} TermEntry() : subjectField(0) {} void clear() { english.clear(); target.clear(); definition.clear(); subjectField = 0; } }; /** * Internal representation of glossary. * * We store only data we need (i.e. only subset of TBX format) * * @short Internal representation of glossary * @author Nick Shaforostoff */ class Glossary: public QObject { Q_OBJECT public: explicit Glossary(QObject* parent); ~Glossary() {} QString path() const { return m_path; } bool isClean() { return m_clean; } QList idsForLangWord(const QString& lang, const QString& word) const; QByteArray id(int index) const; QStringList terms(const QByteArray& id, const QString& lang) const; void setTerm(const QByteArray& id, QString lang, int i, const QString& term); void rmTerm(const QByteArray& id, QString lang, int i); QString subjectField(const QByteArray& id, const QString& lang = QString()) const; void setSubjectField(const QByteArray& id, const QString& lang, const QString& value); QString definition(const QByteArray& id, const QString& lang = QString()) const; void setDefinition(const QByteArray& id, const QString& lang, const QString& value); private: QString descrip(const QByteArray& id, const QString& lang, const QString& type) const; void setDescrip(const QByteArray& id, QString lang, const QString& type, const QString& value); public: QStringList subjectFields() const; int size() const { return m_entries.size(); } void clear(); //disk bool load(const QString&); bool save(); //in-memory changing QByteArray generateNewId(); void append(const QString& _english, const QString& _target); void removeEntry(const QByteArray& id); void forceChangeSignal() { emit changed(); } void setClean(bool); QByteArray append(const QStringList& sourceTerms, const QStringList& targetTerms); //general void hashTermEntry(const QDomElement&); void unhashTermEntry(const QDomElement&); signals: void changed(); void loaded(); private: QString m_path; mutable QDomDocument m_doc; QDomNodeList m_entries; QMap m_entriesById; QList m_idsForEntriesById; QMap< QString, QMultiHash > idsByLangWord; QMultiHash wordHash_; QList termList_; QMap< QString, QMultiHash > langWordEntry_; QStringList subjectFields_;//first entry should be empty //for delayed saving QStringList addedIds_; QStringList changedIds_; QList removedIds; bool m_clean; }; /** * @short MVC wrapper around Glossary */ class GlossaryModel: public QAbstractListModel { Q_OBJECT public: enum Columns { ID = 0, English, Target, SubjectField, GlossaryModelColumnCount }; explicit GlossaryModel(QObject* parent/*, Glossary* glossary*/); ~GlossaryModel() {} //QModelIndex index (int row, int column, const QModelIndex & parent = QModelIndex() ) const; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; //Qt::ItemFlags flags(const QModelIndex&) const; bool canFetchMore(const QModelIndex& parent) const override; void fetchMore(const QModelIndex& parent) override; bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; //bool insertRows(int row,int count,const QModelIndex& parent=QModelIndex()); QByteArray appendRow(const QString& _english, const QString& _target); public slots: void forceReset(); private: int m_visibleCount; Glossary* m_glossary; //taken from Project::instance()->glossary() }; class GlossarySortFilterProxyModel: public QSortFilterProxyModel { Q_OBJECT public: explicit GlossarySortFilterProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} Qt::ItemFlags flags(const QModelIndex&) const override { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } void fetchMore(const QModelIndex& parent) override; public slots: void setFilterRegExp(const QString& s); }; } #endif diff --git a/src/glossary/glossaryview.cpp b/src/glossary/glossaryview.cpp index 7430c1a..7394390 100644 --- a/src/glossary/glossaryview.cpp +++ b/src/glossary/glossaryview.cpp @@ -1,208 +1,209 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "glossaryview.h" #include "lokalize_debug.h" #include "glossary.h" #include "project.h" #include "catalog.h" #include "flowlayout.h" #include "glossarywindow.h" #include "stemming.h" #include #include #include #include #include #include #include using namespace GlossaryNS; GlossaryView::GlossaryView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18nc("@title:window", "Glossary"), parent) , m_browser(new QScrollArea(this)) , m_catalog(catalog) , m_flowLayout(new FlowLayout(FlowLayout::glossary,/*who gets signals*/this, actions, 0, 10)) , m_glossary(Project::instance()->glossary()) , m_rxClean(Project::instance()->markup() % '|' % Project::instance()->accel()) //cleaning regexp; NOTE isEmpty()? , m_rxSplit(QStringLiteral("\\W|\\d"))//splitting regexp , m_currentIndex(-1) , m_normTitle(i18nc("@title:window", "Glossary")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) { setObjectName(QStringLiteral("glossaryView")); QWidget* w = new QWidget(m_browser); m_browser->setWidget(w); m_browser->setWidgetResizable(true); w->setLayout(m_flowLayout); w->show(); setToolTip(i18nc("@info:tooltip", "

Translations for common terms appear here.

" "

Press shortcut displayed near the term to insert its translation.

" "

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

")); setWidget(m_browser); m_browser->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); m_browser->setAutoFillBackground(true); m_browser->setBackgroundRole(QPalette::Background); m_rxClean.setMinimal(true); connect(m_glossary, &Glossary::changed, this, QOverload<>::of(&GlossaryView::slotNewEntryDisplayed), Qt::QueuedConnection); } GlossaryView::~GlossaryView() { } //TODO define new term by dragging some text. // void GlossaryView::dragEnterEvent(QDragEnterEvent* event) // { // /* if(event->mimeData()->hasUrls() && event->mimeData()->urls().first().path().endsWith(".po")) // { // event->acceptProposedAction(); // };*/ // } // // void GlossaryView::dropEvent(QDropEvent *event) // { // event->acceptProposedAction();*/ // } void GlossaryView::slotNewEntryDisplayed() { slotNewEntryDisplayed(DocPosition()); } void GlossaryView::slotNewEntryDisplayed(DocPosition pos) { //qCWarning(LOKALIZE_LOG)<<"\n\n\n\nstart"<numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection //if (!toggleViewAction()->isChecked()) // return; Glossary& glossary = *m_glossary; QString source = m_catalog->source(pos); QString sourceLowered = source.toLower(); QString msg = sourceLowered; msg.remove(m_rxClean); QString msgStemmed; // QRegExp accel(Project::instance()->accel()); // qCWarning(LOKALIZE_LOG)<accel()<sourceLangCode(); QList termIds; foreach (const QString& w, msg.split(m_rxSplit, QString::SkipEmptyParts)) { QString word = stem(sourceLangCode, w); QList indexes = glossary.idsForLangWord(sourceLangCode, word); //if (indexes.size()) //qCWarning(LOKALIZE_LOG)<<"found entry for:" <clearTerms(); bool found = false; //m_flowLayout->setEnabled(false); foreach (const QByteArray& termId, termIds.toSet()) { // now check which of them are really hits... foreach (const QString& enTerm, glossary.terms(termId, sourceLangCode)) { // ...and if so, which part of termEn list we must thank for match ... bool ok = msg.contains(enTerm); //,//Qt::CaseInsensitive //we lowered terms on load if (!ok) { QString enTermStemmed; foreach (const QString& word, enTerm.split(m_rxSplit, QString::SkipEmptyParts)) enTermStemmed += stem(sourceLangCode, word) + ' '; ok = msgStemmed.contains(enTermStemmed); } if (ok) { //insert it into label found = true; int pos = sourceLowered.indexOf(enTerm); m_flowLayout->addTerm(enTerm, termId,/*uppercase*/pos != -1 && source.at(pos).isUpper()); break; } } } //m_flowLayout->setEnabled(true); if (!found) clear(); else if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(true); } void GlossaryView::clear() { if (m_hasInfo) { m_flowLayout->clearTerms(); m_hasInfo = false; setWindowTitle(m_normTitle); } } diff --git a/src/glossary/glossaryview.h b/src/glossary/glossaryview.h index 060d780..a1ec8b3 100644 --- a/src/glossary/glossaryview.h +++ b/src/glossary/glossaryview.h @@ -1,83 +1,84 @@ /* **************************************************************************** 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 GLOSSARYVIEW_H #define GLOSSARYVIEW_H #include #include #include class Catalog; class FlowLayout; class QDragEnterEvent; class QDropEvent; class QEvent; class QAction; class QFrame; class QScrollArea; #include namespace GlossaryNS { class Glossary; #define GLOSSARY_SHORTCUTS 11 class GlossaryView: public QDockWidget { Q_OBJECT public: explicit GlossaryView(QWidget*, Catalog*, const QVector&); ~GlossaryView(); // void dragEnterEvent(QDragEnterEvent* event); // void dropEvent(QDropEvent*); // bool event(QEvent*); public slots: //plural messages usually contain the same words... void slotNewEntryDisplayed(); void slotNewEntryDisplayed(DocPosition pos);//a little hacky, but... :) signals: void termInsertRequested(const QString&); private: void clear(); private: QScrollArea* m_browser; Catalog* m_catalog; FlowLayout *m_flowLayout; Glossary* m_glossary; QRegExp m_rxClean; QRegExp m_rxSplit; int m_currentIndex; QString m_normTitle; QString m_hasInfoTitle; bool m_hasInfo; }; } #endif diff --git a/src/glossary/glossarywindow.cpp b/src/glossary/glossarywindow.cpp index 92b5887..521c2a0 100644 --- a/src/glossary/glossarywindow.cpp +++ b/src/glossary/glossarywindow.cpp @@ -1,566 +1,567 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "glossarywindow.h" #include "lokalize_debug.h" #include "glossary.h" #include "project.h" #include "languagelistmodel.h" #include "ui_termedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace GlossaryNS; //BEGIN GlossaryTreeView GlossaryTreeView::GlossaryTreeView(QWidget *parent) : QTreeView(parent) { setSortingEnabled(true); sortByColumn(GlossaryModel::English, Qt::AscendingOrder); setItemsExpandable(false); setAllColumnsShowFocus(true); /* setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows);*/ } static QByteArray modelIndexToId(const QModelIndex& item) { return item.sibling(item.row(), 0).data(Qt::DisplayRole).toByteArray(); } void GlossaryTreeView::currentChanged(const QModelIndex& current, const QModelIndex&/* previous*/) { if (current.isValid()) { //QModelIndex item=static_cast(model())->mapToSource(current); //emit currentChanged(item.row()); emit currentChanged(modelIndexToId(current)); scrollTo(current); } } void GlossaryTreeView::selectRow(int i) { QSortFilterProxyModel* proxyModel = static_cast(model()); GlossaryModel* sourceModel = static_cast(proxyModel->sourceModel()); //sourceModel->forceReset(); setCurrentIndex(proxyModel->mapFromSource(sourceModel->index(i, 0))); } //END GlossaryTreeView //BEGIN SubjectFieldModel //typedef QStringListModel SubjectFieldModel; #if 0 class SubjectFieldModel: public QAbstractItemModel { public: //Q_OBJECT SubjectFieldModel(QObject* parent); ~SubjectFieldModel() {} QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; QModelIndex parent(const QModelIndex&) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; int columnCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const; bool setData(const QModelIndex&, const QVariant&, int role = Qt::EditRole); bool setItemData(const QModelIndex& index, const QMap& roles); bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()); Qt::ItemFlags flags(const QModelIndex&) const; /*private: Catalog* m_catalog;*/ }; inline SubjectFieldModel::SubjectFieldModel(QObject* parent) : QAbstractItemModel(parent) // , m_catalog(catalog) { } QModelIndex SubjectFieldModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } Qt::ItemFlags SubjectFieldModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled; } QModelIndex SubjectFieldModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int SubjectFieldModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return 1; } /* inline Qt::ItemFlags SubjectFieldModel::flags ( const QModelIndex & index ) const { if (index.column()==FuzzyFlag) return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; return QAbstractItemModel::flags(index); }*/ int SubjectFieldModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return Project::instance()->glossary()->subjectFields.size(); } QVariant SubjectFieldModel::data(const QModelIndex& index, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole) return Project::instance()->glossary()->subjectFields.at(index.row()); return QVariant(); } bool SubjectFieldModel::insertRows(int row, int count, const QModelIndex& parent) { beginInsertRows(parent, row, row + count - 1); QStringList& subjectFields = Project::instance()->glossary()->subjectFields; while (--count >= 0) subjectFields.insert(row + count, QString()); endInsertRows(); return true; } bool SubjectFieldModel::setData(const QModelIndex& index, const QVariant& value, int role) { qCDebug(LOKALIZE_LOG) << role; QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = value.toString(); return true; } bool SubjectFieldModel::setItemData(const QModelIndex& index, const QMap& roles) { if (roles.contains(Qt::EditRole)) { QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = roles.value(Qt::EditRole).toString(); } return true; } #endif //END SubjectFieldModel //BEGIN GlossaryWindow GlossaryWindow::GlossaryWindow(QWidget *parent) : KMainWindow(parent) , m_browser(new GlossaryTreeView(this)) , m_proxyModel(new GlossarySortFilterProxyModel(this)) , m_reactOnSignals(true) { //setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, false); QSplitter* splitter = new QSplitter(Qt::Horizontal, this); setCentralWidget(splitter); m_proxyModel->setFilterKeyColumn(-1); m_proxyModel->setDynamicSortFilter(true);; GlossaryModel* model = new GlossaryModel(this); m_proxyModel->setSourceModel(model); m_browser->setModel(m_proxyModel); m_browser->setUniformRowHeights(true); m_browser->setAutoScroll(true); m_browser->setColumnHidden(GlossaryModel::ID, true); m_browser->setColumnWidth(GlossaryModel::English, m_browser->columnWidth(GlossaryModel::English) * 2); //man this is HACK y m_browser->setColumnWidth(GlossaryModel::Target, m_browser->columnWidth(GlossaryModel::Target) * 2); m_browser->setAlternatingRowColors(true); //left QWidget* w = new QWidget(splitter); QVBoxLayout* layout = new QVBoxLayout(w); m_filterEdit = new QLineEdit(w); m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setPlaceholderText(i18n("Quick search...")); m_filterEdit->setFocus(); m_filterEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); new QShortcut(Qt::CTRL + Qt::Key_L, this, SLOT(setFocus()), 0, Qt::WidgetWithChildrenShortcut); connect(m_filterEdit, &QLineEdit::textChanged, m_proxyModel, &GlossaryNS::GlossarySortFilterProxyModel::setFilterRegExp); layout->addWidget(m_filterEdit); layout->addWidget(m_browser); { QPushButton* addBtn = new QPushButton(w); connect(addBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::newTermEntry)); QPushButton* rmBtn = new QPushButton(w); connect(rmBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::rmTermEntry)); KGuiItem::assign(addBtn, KStandardGuiItem::add()); KGuiItem::assign(rmBtn, KStandardGuiItem::remove()); QPushButton* restoreBtn = new QPushButton(i18nc("@action:button reloads glossary from disk", "Restore from disk"), w); restoreBtn->setToolTip(i18nc("@info:tooltip", "Reload glossary from disk, discarding any changes")); connect(restoreBtn, &QPushButton::clicked, this, &GlossaryWindow::restore); QWidget* btns = new QWidget(w); QHBoxLayout* btnsLayout = new QHBoxLayout(btns); btnsLayout->addWidget(addBtn); btnsLayout->addWidget(rmBtn); btnsLayout->addWidget(restoreBtn); layout->addWidget(btns); //QWidget::setTabOrder(m_browser,addBtn); QWidget::setTabOrder(addBtn, rmBtn); QWidget::setTabOrder(rmBtn, restoreBtn); QWidget::setTabOrder(restoreBtn, m_filterEdit); } QWidget::setTabOrder(m_filterEdit, m_browser); splitter->addWidget(w); //right m_editor = new QWidget(splitter); m_editor->hide(); Ui_TermEdit ui_termEdit; ui_termEdit.setupUi(m_editor); splitter->addWidget(m_editor); Project* project = Project::instance(); m_sourceTermsModel = new TermsListModel(project->glossary(), project->sourceLangCode(), this); m_targetTermsModel = new TermsListModel(project->glossary(), project->targetLangCode(), this); ui_termEdit.sourceTermsView->setModel(m_sourceTermsModel); ui_termEdit.targetTermsView->setModel(m_targetTermsModel); connect(ui_termEdit.addEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::addTerm); connect(ui_termEdit.remEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::rmTerms); connect(ui_termEdit.addTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::addTerm); connect(ui_termEdit.remTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::rmTerms); m_sourceTermsView = ui_termEdit.sourceTermsView; m_targetTermsView = ui_termEdit.targetTermsView; m_subjectField = ui_termEdit.subjectField; m_definition = ui_termEdit.definition; m_definitionLang = ui_termEdit.definitionLang; //connect (m_english,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_target,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(editingFinished()),this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(textChanged()),this,SLOT(applyEntryChange())); //connect (m_subjectField,SIGNAL(editTextChanged(QString)),this,SLOT(applyEntryChange())); connect(m_subjectField->lineEdit(), &QLineEdit::editingFinished, this, &GlossaryWindow::applyEntryChange); //m_subjectField->addItems(Project::instance()->glossary()->subjectFields()); //m_subjectField->setModel(new SubjectFieldModel(this)); QStringList subjectFields = Project::instance()->glossary()->subjectFields(); qSort(subjectFields); QStringListModel* subjectFieldsModel = new QStringListModel(this); subjectFieldsModel->setStringList(subjectFields); m_subjectField->setModel(subjectFieldsModel); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::currentChanged); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::showEntryInEditor); connect(m_definitionLang, QOverload::of(&KComboBox::activated), this, &GlossaryWindow::showDefinitionForLang); m_definitionLang->setModel(LanguageListModel::emptyLangInstance()->sortModel()); m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(m_defLang));//empty lang //TODO //connect(m_targetTermsModel,SIGNAL(dataChanged(QModelIndex,QModelIndex)),m_browser,SLOT(setFocus())); setAutoSaveSettings(QLatin1String("GlossaryWindow"), true); //Glossary* glossary=Project::instance()->glossary(); /*setCaption(i18nc("@title:window","Glossary"), !glossary->changedIds.isEmpty()||!glossary->addedIds.isEmpty()||!glossary->removedIds.isEmpty()); */ } GlossaryWindow::~GlossaryWindow() { } void GlossaryWindow::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void GlossaryWindow::showEntryInEditor(const QByteArray& id) { if (m_editor->isVisible()) applyEntryChange(); else m_editor->show(); m_id = id; m_reactOnSignals = false; Project* project = Project::instance(); Glossary* glossary = project->glossary(); m_subjectField->setCurrentItem(glossary->subjectField(id),/*insert*/true); QStringList langsToTry = QStringList(m_defLang) << QStringLiteral("en") << QStringLiteral("en_US") << project->targetLangCode(); foreach (const QString& lang, langsToTry) { QString d = glossary->definition(m_id, lang); if (!d.isEmpty()) { if (m_defLang != lang) m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(lang)); m_defLang = lang; break; } } m_definition->setPlainText(glossary->definition(m_id, m_defLang)); m_sourceTermsModel->setEntry(id); m_targetTermsModel->setEntry(id); //m_sourceTermsModel->setStringList(glossary->terms(id,project->sourceLangCode())); //m_targetTermsModel->setStringList(glossary->terms(id,project->targetLangCode())); m_reactOnSignals = true; } void GlossaryWindow::currentChanged(int i) { Q_UNUSED(i); m_reactOnSignals = false; m_editor->show(); m_reactOnSignals = true; } void GlossaryWindow::showDefinitionForLang(int langModelIndex) { applyEntryChange(); m_defLang = LanguageListModel::emptyLangInstance()->langCodeForSortModelRow(langModelIndex); m_definition->setPlainText(Project::instance()->glossary()->definition(m_id, m_defLang)); } void GlossaryWindow::applyEntryChange() { if (!m_reactOnSignals || !m_browser->currentIndex().isValid()) return; QByteArray id = m_id; //modelIndexToId(m_browser->currentIndex()); Project* project = Project::instance(); Glossary* glossary = project->glossary(); if (m_subjectField->currentText() != glossary->subjectField(id)) glossary->setSubjectField(id, QString(), m_subjectField->currentText()); if (m_definition->toPlainText() != glossary->definition(id, m_defLang)) glossary->setDefinition(id, m_defLang, m_definition->toPlainText()); //HACK to force finishing of the listview editing QWidget* prevFocusWidget = QApplication::focusWidget(); m_browser->setFocus(); if (prevFocusWidget) prevFocusWidget->setFocus(); // QSortFilterProxyModel* proxyModel=static_cast(model()); //GlossaryModel* sourceModel=static_cast(m_proxyModel->sourceModel()); const QModelIndex& idx = m_proxyModel->mapToSource(m_browser->currentIndex()); if (!idx.isValid()) return; //TODO display filename, optionally stripped like for filetab names setCaption(i18nc("@title:window", "Glossary"), !glossary->isClean()); } void GlossaryWindow::selectEntry(const QByteArray& id) { //let it fetch the rows QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers | QEventLoop::WaitForMoreEvents, 100); QModelIndexList items = m_proxyModel->match(m_proxyModel->index(0, 0), Qt::DisplayRole, QVariant(id), 1, 0); if (items.count()) { m_browser->setCurrentIndex(items.first()); m_browser->scrollTo(items.first(), QAbstractItemView::PositionAtCenter); //qCDebug(LOKALIZE_LOG)<setCurrentIndex(QModelIndex()); showEntryInEditor(id); //qCDebug(LOKALIZE_LOG)<(m_proxyModel->sourceModel()); QByteArray id = sourceModel->appendRow(_english, _target); selectEntry(id); } void GlossaryWindow::rmTermEntry() { rmTermEntry(-1); } void GlossaryWindow::rmTermEntry(int i) { setCaption(i18nc("@title:window", "Glossary"), true); //QSortFilterProxyModel* proxyModel=static_cast(model()); GlossaryModel* sourceModel = static_cast(m_proxyModel->sourceModel()); if (i == -1) { //NOTE actually we should remove selected items, not current one const QModelIndex& current = m_browser->currentIndex(); if (!current.isValid()) return; i = m_proxyModel->mapToSource(current).row(); } sourceModel->removeRow(i); } void GlossaryWindow::restore() { setCaption(i18nc("@title:window", "Glossary"), false); Glossary* glossary = Project::instance()->glossary(); glossary->load(glossary->path()); m_reactOnSignals = false; showEntryInEditor(m_id); m_reactOnSignals = true; } bool GlossaryWindow::save() { //TODO add error message return Project::instance()->glossary()->save(); } bool GlossaryWindow::queryClose() { Glossary* glossary = Project::instance()->glossary(); applyEntryChange(); if (glossary->isClean()) return true; switch (KMessageBox::warningYesNoCancel(this, i18nc("@info", "The glossary contains unsaved changes.\n\ Do you want to save your changes or discard them?"), i18nc("@title:window", "Warning"), KStandardGuiItem::save(), KStandardGuiItem::discard())) { case KMessageBox::Yes: return save(); case KMessageBox::No: restore(); return true; default: return false; } } //END GlossaryWindow void TermsListModel::setEntry(const QByteArray& id) { m_id = id; QStringList terms = m_glossary->terms(m_id, m_lang); terms.append(QString()); //allow adding new terms setStringList(terms); } bool TermsListModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_UNUSED(role); m_glossary->setTerm(m_id, m_lang, index.row(), value.toString()); setEntry(m_id); //allow adding new terms return true; } bool TermsListModel::removeRows(int row, int count, const QModelIndex& parent) { Q_UNUSED(count) if (row == rowCount() - 1) return false;// cannot delete non-existing item m_glossary->rmTerm(m_id, m_lang, row); return QStringListModel::removeRows(row, 1, parent); } void TermListView::addTerm() { setCurrentIndex(model()->index(model()->rowCount() - 1, 0)); edit(currentIndex()); } void TermListView::rmTerms() { foreach (const QModelIndex& row, selectionModel()->selectedRows()) model()->removeRow(row.row()); } diff --git a/src/glossary/glossarywindow.h b/src/glossary/glossarywindow.h index 1f824c3..94a3897 100644 --- a/src/glossary/glossarywindow.h +++ b/src/glossary/glossarywindow.h @@ -1,173 +1,174 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef GLOSSARYWINDOW_H #define GLOSSARYWINDOW_H #include #include #include #include #include class QListView; //class KTextEdit; class QLineEdit; class KComboBox; //class QStringListModel; class AuxTextEdit: public KTextEdit { Q_OBJECT public: explicit AuxTextEdit(QWidget* parent = nullptr): KTextEdit(parent) {} void focusOutEvent(QFocusEvent* e) override { Q_UNUSED(e); emit editingFinished(); } signals: void editingFinished(); }; class TermListView: public QListView { Q_OBJECT public: explicit TermListView(QWidget* parent = nullptr): QListView(parent) {} public slots: void rmTerms(); void addTerm(); }; namespace GlossaryNS { class GlossaryTreeView; class Glossary; class TermsListModel; class GlossarySortFilterProxyModel; class GlossaryWindow: public KMainWindow { Q_OBJECT public: explicit GlossaryWindow(QWidget *parent = nullptr); ~GlossaryWindow() override; bool queryClose() override; public slots: void currentChanged(int); void showEntryInEditor(const QByteArray& id); void showDefinitionForLang(int); void newTermEntry(QString _english, QString _target); void newTermEntry(); void rmTermEntry(int i); void rmTermEntry(); void restore(); bool save(); void applyEntryChange(); void selectEntry(const QByteArray& id); void setFocus(); private: QWidget* m_editor; GlossaryTreeView* m_browser; TermsListModel* m_sourceTermsModel; TermsListModel* m_targetTermsModel; GlossarySortFilterProxyModel* m_proxyModel; QLineEdit* m_filterEdit; KComboBox* m_subjectField; KTextEdit* m_definition; KComboBox* m_definitionLang; QListView* m_sourceTermsView; QListView* m_targetTermsView; bool m_reactOnSignals; QByteArray m_id; QString m_defLang; }; class GlossaryTreeView: public QTreeView { Q_OBJECT public: explicit GlossaryTreeView(QWidget *parent = nullptr); ~GlossaryTreeView() override {} void currentChanged(const QModelIndex& current, const QModelIndex& previous) override; void selectRow(int i); signals: void currentChanged(int); void currentChanged(const QByteArray&); void currentChanged(const QByteArray& prev, const QByteArray& current); }; class TermsListModel: public QStringListModel { Q_OBJECT public: TermsListModel(Glossary* glossary, const QString& lang, QObject* parent = nullptr): QStringListModel(parent), m_glossary(glossary), m_lang(lang) {} bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; public slots: void setEntry(const QByteArray& id); private: Glossary* m_glossary; QString m_lang; QByteArray m_id; }; /* class GlossaryItemDelegate : public QItemDelegate//KFileItemDelegate { Q_OBJECT public: GlossaryItemDelegate(QObject *parent=0) : QItemDelegate(parent) {} ~GlossaryItemDelegate(){} bool editorEvent (QEvent* event,QAbstractItemModel* model,const QStyleOptionViewItem& option,const QModelIndex& index); signals: void selected(const QUrl&); void newWindowOpenRequested(const QUrl&); }; */ } #endif diff --git a/src/glossary/tbxparser_obsolete.cpp b/src/glossary/tbxparser_obsolete.cpp index 43a48cc..c5f70bf 100644 --- a/src/glossary/tbxparser_obsolete.cpp +++ b/src/glossary/tbxparser_obsolete.cpp @@ -1,124 +1,125 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalize_debug.h" #include "tbxparser.h" #include "glossary.h" #include "project.h" using namespace GlossaryNS; bool TbxParser::startDocument() { m_state = null; m_lang = langNull; return true; } bool TbxParser::startElement(const QString&, const QString&, const QString& qName, const QXmlAttributes& attr) { if (qName == "langSet") { if (attr.value("xml:lang").startsWith(QLatin1String("en"))) m_lang = langEn; else if (attr.value("xml:lang") == Project::instance()->langCode()) m_lang = langTarget; else m_lang = langNull; } else if (qName == "term") { m_state = term; } else if (qName == "termEntry") { m_termEn.clear(); m_termOther.clear(); m_entry.clear(); m_entry.id = attr.value("id"); } else if (qName == "descrip") { if (attr.value("type") == "definition") m_state = descripDefinition; else if (attr.value("type") == "subjectField") m_state = descripSubjectField; } return true; } bool TbxParser::endElement(const QString&, const QString&, const QString& qName) { if (qName == "term") { if (m_lang == langEn) { m_entry.english << m_termEn; m_termEn.clear(); m_entry.english.last().squeeze(); } else if (m_lang == langTarget) { m_entry.target << m_termOther; m_termOther.clear(); m_entry.target.last().squeeze(); } } else if (qName == "descrip") { if (m_state == descripSubjectField && !m_subjectField.isEmpty()) { m_entry.subjectField = Project::instance()->glossary()->subjectFields.indexOf(m_subjectField); if (m_entry.subjectField == -1) { //got this field value for the first time m_entry.subjectField = Project::instance()->glossary()->subjectFields.size(); Project::instance()->glossary()->subjectFields << m_subjectField; } m_subjectField.clear(); } } else if (qName == "termEntry") { //sanity check --maybe this entry is only for another language? if (m_entry.target.isEmpty() || m_entry.english.isEmpty()) return true; int index = m_glossary->termList.count(); m_glossary->termList.append(m_entry); m_glossary->hashTermEntry(index); m_entry.clear(); } m_state = null; return true; } bool TbxParser::characters(const QString & ch) { if (m_state == term) { if (m_lang == langEn) m_termEn += ch.toLower(); //this is important else if (m_lang == langTarget) m_termOther += ch; } else if (m_state == descripDefinition) m_entry.definition += ch; else if (m_state == descripSubjectField) m_subjectField += ch; return true; } diff --git a/src/glossary/tbxparser_obsolete.h b/src/glossary/tbxparser_obsolete.h index 9a47412..a264a41 100644 --- a/src/glossary/tbxparser_obsolete.h +++ b/src/glossary/tbxparser_obsolete.h @@ -1,83 +1,84 @@ /* **************************************************************************** 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 TBXPARSER_H #define TBXPARSER_H #include #include "glossary.h" namespace GlossaryNS { /** * loads only data we need to store in memory * e.g. skips entries for languages other than en * and current project's target language * * @short TBX glossary parser * @author Nick Shaforostoff */ class TbxParser : public QXmlDefaultHandler { enum State { //localstate for getting chars into right place null = 0, // termGrp, term, descripDefinition, descripSubjectField }; enum Lang { langNull = 0, langEn, langTarget }; public: explicit TbxParser(Glossary* glossary) : QXmlDefaultHandler() , m_glossary(glossary) {} ~TbxParser() {} bool startDocument(); bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&); bool endElement(const QString&, const QString&, const QString&); bool characters(const QString&); private: // bool inTermTag:1; State m_state: 8; Lang m_lang: 8; QString m_termEn; QString m_termOther; TermEntry m_entry; QString m_subjectField; Glossary* m_glossary; }; } #endif diff --git a/src/lokalizemainwindow.cpp b/src/lokalizemainwindow.cpp index 6e4b363..5443f13 100644 --- a/src/lokalizemainwindow.cpp +++ b/src/lokalizemainwindow.cpp @@ -1,1083 +1,1084 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-2015 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalizemainwindow.h" #include "lokalize_debug.h" #include "actionproxy.h" #include "editortab.h" #include "projecttab.h" #include "tmtab.h" #include "jobs.h" #include "filesearchtab.h" #include "prefs_lokalize.h" // #define WEBQUERY_ENABLE #include "project.h" #include "projectmodel.h" #include "projectlocal.h" #include "prefs.h" #include "tools/widgettextcaptureconfig.h" #include "multieditoradaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include LokalizeMainWindow::LokalizeMainWindow() : KXmlGuiWindow() , m_mdiArea(new LokalizeMdiArea) , m_prevSubWindow(0) , m_projectSubWindow(0) , m_translationMemorySubWindow(0) , m_editorActions(new QActionGroup(this)) , m_managerActions(new QActionGroup(this)) , m_spareEditor(new EditorTab(this, false)) , m_multiEditorAdaptor(new MultiEditorAdaptor(m_spareEditor)) , m_projectScriptingPlugin(0) { m_spareEditor->hide(); m_mdiArea->setViewMode(QMdiArea::TabbedView); m_mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); m_mdiArea->setDocumentMode(true); m_mdiArea->setTabsMovable(true); m_mdiArea->setTabsClosable(true); setCentralWidget(m_mdiArea); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &LokalizeMainWindow::slotSubWindowActivated); setupActions(); //prevent relayout of dockwidgets m_mdiArea->setOption(QMdiArea::DontMaximizeSubWindowOnActivation, true); connect(Project::instance(), QOverload::of(&Project::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_), Qt::QueuedConnection); connect(Project::instance(), &Project::configChanged, this, &LokalizeMainWindow::projectSettingsChanged); connect(Project::instance(), &Project::closed, this, &LokalizeMainWindow::closeProject); showProjectOverview(); showTranslationMemory(); //fix for #342558 for (int i = ID_STATUS_CURRENT; i <= ID_STATUS_ISFUZZY; i++) { m_statusBarLabels.append(new QLabel()); statusBar()->insertWidget(i, m_statusBarLabels.last(), 2); } setAttribute(Qt::WA_DeleteOnClose, true); if (!qApp->isSessionRestored()) { KConfig config; KConfigGroup stateGroup(&config, "State"); readProperties(stateGroup); } registerDBusAdaptor(); QTimer::singleShot(0, this, &LokalizeMainWindow::initLater); } void LokalizeMainWindow::initLater() { if (!m_prevSubWindow && m_projectSubWindow) slotSubWindowActivated(m_projectSubWindow); if (!Project::instance()->isTmSupported()) { KNotification* notification = new KNotification("NoSqlModulesAvailable", this); notification->setText(i18nc("@info", "No Qt Sql modules were found. Translation memory will not work.")); notification->sendEvent(); } } LokalizeMainWindow::~LokalizeMainWindow() { TM::cancelAllJobs(); KConfig config; KConfigGroup stateGroup(&config, "State"); if (!m_lastEditorState.isEmpty()) { stateGroup.writeEntry("DefaultDockWidgets", m_lastEditorState); } saveProjectState(stateGroup); m_multiEditorAdaptor->deleteLater(); //Disconnect the signals pointing to this MainWindow object QMdiSubWindow* sw; for (int i = 0; i < m_fileToEditor.values().count(); i++) { sw = m_fileToEditor.values().at(i); disconnect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); EditorTab* w = static_cast(sw->widget()); disconnect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); disconnect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); disconnect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); } qCWarning(LOKALIZE_LOG) << "MainWindow destroyed"; } void LokalizeMainWindow::slotSubWindowActivated(QMdiSubWindow* w) { //QTime aaa;aaa.start(); if (!w || m_prevSubWindow == w) return; w->setUpdatesEnabled(true); //QTBUG-23289 if (m_prevSubWindow) { m_prevSubWindow->setUpdatesEnabled(false); LokalizeSubwindowBase* prevEditor = static_cast(m_prevSubWindow->widget()); prevEditor->hideDocks(); guiFactory()->removeClient(prevEditor->guiClient()); prevEditor->statusBarItems.unregisterStatusBar(); if (qobject_cast(prevEditor)) { EditorTab* w = static_cast(prevEditor); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); } /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { disconnect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); } } */ } LokalizeSubwindowBase* editor = static_cast(w->widget()); editor->reloadUpdatedXML(); if (qobject_cast(editor)) { EditorTab* w = static_cast(editor); w->setProperFocus(); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); m_multiEditorAdaptor->setEditorTab(w); // connect(m_multiEditorAdaptor,SIGNAL(srcFileOpenRequested(QString,int)),this,SLOT(showTM())); /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { connect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); }*/ QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), w->currentFilePath()); emit editorActivated(); } else if (w == m_projectSubWindow && m_projectSubWindow) { QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), Project::instance()->path()); } editor->showDocks(); editor->statusBarItems.registerStatusBar(statusBar(), m_statusBarLabels); guiFactory()->addClient(editor->guiClient()); m_prevSubWindow = w; //qCWarning(LOKALIZE_LOG)<<"finished"< editors = m_mdiArea->subWindowList(); int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!qobject_cast(editors.at(i)->widget())) continue; if (!static_cast(editors.at(i)->widget())->queryClose()) return false; } bool ok = Project::instance()->queryCloseForAuxiliaryWindows(); if (ok) { QThreadPool::globalInstance()->clear(); Project::instance()->model()->threadPool()->clear(); } return ok; } EditorTab* LokalizeMainWindow::fileOpen_(QString filePath, const bool setAsActive) { return fileOpen(filePath, 0, setAsActive); } EditorTab* LokalizeMainWindow::fileOpen(QString filePath, int entry, bool setAsActive, const QString& mergeFile, bool silent) { if (filePath.length()) { FileToEditor::const_iterator it = m_fileToEditor.constFind(filePath); if (it != m_fileToEditor.constEnd()) { qCWarning(LOKALIZE_LOG) << "already opened:" << filePath; if (QMdiSubWindow* sw = it.value()) { m_mdiArea->setActiveSubWindow(sw); return static_cast(sw->widget()); } } } QByteArray state = m_lastEditorState; EditorTab* w = new EditorTab(this); QMdiSubWindow* sw = 0; //create QMdiSubWindow BEFORE fileOpen() because it causes some strange QMdiArea behaviour otherwise if (filePath.length()) sw = m_mdiArea->addSubWindow(w); QString suggestedDirPath; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (activeSW && qobject_cast(activeSW->widget())) { QString fp = static_cast(activeSW->widget())->currentFilePath(); if (fp.length()) suggestedDirPath = QFileInfo(fp).absolutePath(); } if (!w->fileOpen(filePath, suggestedDirPath, silent)) { if (sw) { m_mdiArea->removeSubWindow(sw); sw->deleteLater(); } w->deleteLater(); return 0; } if (!sw) sw = m_mdiArea->addSubWindow(w); w->showMaximized(); sw->showMaximized(); if (!state.isEmpty()) { w->restoreState(QByteArray::fromBase64(state)); m_lastEditorState = state; } else { //Dummy restore to "initialize" widgets w->restoreState(w->saveState()); } if (entry/* || offset*/) w->gotoEntry(DocPosition(entry/*, DocPosition::Target, 0, offset*/)); if (setAsActive) { m_toBeActiveSubWindow = sw; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } else { m_mdiArea->setActiveSubWindow(activeSW); sw->setUpdatesEnabled(false); //QTBUG-23289 } if (!mergeFile.isEmpty()) w->mergeOpen(mergeFile); m_openRecentFileAction->addUrl(QUrl::fromLocalFile(filePath));//(w->currentUrl()); connect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); connect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); connect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); filePath = w->currentFilePath(); QStringRef fnSlashed = filePath.midRef(filePath.lastIndexOf('/')); FileToEditor::const_iterator i = m_fileToEditor.constBegin(); while (i != m_fileToEditor.constEnd()) { if (i.key().endsWith(fnSlashed)) { static_cast(i.value()->widget())->setFullPathShown(true); w->setFullPathShown(true); } ++i; } m_fileToEditor.insert(filePath, sw); sw->setAttribute(Qt::WA_DeleteOnClose, true); emit editorAdded(); return w; } void LokalizeMainWindow::resetMultiEditorAdaptor() { m_multiEditorAdaptor->setEditorTab(m_spareEditor); //it will be reparented shortly if there are other editors } void LokalizeMainWindow::editorClosed(QObject* obj) { m_fileToEditor.remove(m_fileToEditor.key(static_cast(obj))); } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, const QString& source, const QString& ctxt, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->findEntryBySourceContext(source, ctxt); return w; } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, DocPosition docPos, int selection, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->gotoEntry(docPos, selection); return w; } QObject* LokalizeMainWindow::projectOverview() { if (!m_projectSubWindow) { ProjectTab* w = new ProjectTab(this); m_projectSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_projectSubWindow->showMaximized(); connect(w, QOverload::of(&ProjectTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); connect(w, QOverload::of(&ProjectTab::projectOpenRequested), this, QOverload::of(&LokalizeMainWindow::openProject)); connect(w, QOverload<>::of(&ProjectTab::projectOpenRequested), this, QOverload<>::of(&LokalizeMainWindow::openProject)); connect(w, QOverload::of(&ProjectTab::searchRequested), this, QOverload::of(&LokalizeMainWindow::addFilesToSearch)); } if (m_mdiArea->currentSubWindow() == m_projectSubWindow) return m_projectSubWindow->widget(); return 0; } void LokalizeMainWindow::showProjectOverview() { projectOverview(); m_mdiArea->setActiveSubWindow(m_projectSubWindow); } TM::TMTab* LokalizeMainWindow::showTM() { if (!Project::instance()->isTmSupported()) { KMessageBox::information(nullptr, i18n("TM facility requires SQLite Qt module."), i18n("No SQLite module available")); return 0; } if (!m_translationMemorySubWindow) { TM::TMTab* w = new TM::TMTab(this); m_translationMemorySubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_translationMemorySubWindow->showMaximized(); connect(w, QOverload::of(&TM::TMTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); } m_mdiArea->setActiveSubWindow(m_translationMemorySubWindow); return static_cast(m_translationMemorySubWindow->widget()); } FileSearchTab* LokalizeMainWindow::showFileSearch(bool activate) { EditorTab* precedingEditor = qobject_cast(activeEditor()); if (!m_fileSearchSubWindow) { FileSearchTab* w = new FileSearchTab(this); m_fileSearchSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_fileSearchSubWindow->showMaximized(); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); } if (activate) { m_mdiArea->setActiveSubWindow(m_fileSearchSubWindow); if (precedingEditor) { if (precedingEditor->selectionInSource().length()) static_cast(m_fileSearchSubWindow->widget())->setSourceQuery(precedingEditor->selectionInSource()); if (precedingEditor->selectionInTarget().length()) static_cast(m_fileSearchSubWindow->widget())->setTargetQuery(precedingEditor->selectionInTarget()); } } return static_cast(m_fileSearchSubWindow->widget()); } void LokalizeMainWindow::fileSearchNext() { FileSearchTab* w = showFileSearch(/*activate=*/false); //TODO fill search params based on current selection w->fileSearchNext(); } void LokalizeMainWindow::addFilesToSearch(const QStringList& files) { FileSearchTab* w = showFileSearch(); w->addFilesToSearch(files); } void LokalizeMainWindow::applyToBeActiveSubWindow() { m_mdiArea->setActiveSubWindow(m_toBeActiveSubWindow); } void LokalizeMainWindow::setupActions() { //all operations that can be done after initial setup //(via QTimer::singleShot) go to initLater() QTime aaa; aaa.start(); setStandardToolBarMenuEnabled(true); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* actionCategory; KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), ac); //KActionCategory* config=new KActionCategory(i18nc("@title actions category","Settings"), ac); KActionCategory* glossary = new KActionCategory(i18nc("@title actions category", "Glossary"), ac); KActionCategory* tm = new KActionCategory(i18nc("@title actions category", "Translation Memory"), ac); KActionCategory* proj = new KActionCategory(i18nc("@title actions category", "Project"), ac); actionCategory = file; // File //KStandardAction::open(this, SLOT(fileOpen()), ac); file->addAction(KStandardAction::Open, this, SLOT(fileOpen())); m_openRecentFileAction = KStandardAction::openRecent(this, SLOT(fileOpen(QUrl)), ac); file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows())); //Settings SettingsController* sc = SettingsController::instance(); KStandardAction::preferences(sc, &SettingsController::showSettingsDialog, ac); #define ADD_ACTION_SHORTCUT(_name,_text,_shortcut)\ action = actionCategory->addAction(QStringLiteral(_name));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut ));\ action->setText(_text); //Window //documentBack //KStandardAction::close(m_mdiArea, SLOT(closeActiveSubWindow()), ac); actionCategory = file; ADD_ACTION_SHORTCUT("next-tab", i18n("Next tab"), Qt::CTRL + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activateNextSubWindow); ADD_ACTION_SHORTCUT("prev-tab", i18n("Previous tab"), Qt::CTRL + Qt::SHIFT + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activatePreviousSubWindow); ADD_ACTION_SHORTCUT("prev-active-tab", i18n("Previously active tab"), Qt::CTRL + Qt::Key_BracketLeft) connect(action, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow); //Tools actionCategory = glossary; Project* project = Project::instance(); ADD_ACTION_SHORTCUT("tools_glossary", i18nc("@action:inmenu", "Glossary"), Qt::CTRL + Qt::ALT + Qt::Key_G) connect(action, &QAction::triggered, project, &Project::showGlossary); actionCategory = tm; ADD_ACTION_SHORTCUT("tools_tm", i18nc("@action:inmenu", "Translation memory"), Qt::Key_F7) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showTM); action = tm->addAction("tools_tm_manage", project, SLOT(showTMManager())); action->setText(i18nc("@action:inmenu", "Manage translation memories")); //Project actionCategory = proj; ADD_ACTION_SHORTCUT("project_overview", i18nc("@action:inmenu", "Project overview"), Qt::Key_F4) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showProjectOverview); action = proj->addAction(QStringLiteral("project_configure"), sc, SLOT(projectConfigure())); action->setText(i18nc("@action:inmenu", "Configure project...")); action = proj->addAction(QStringLiteral("project_create"), sc, SLOT(projectCreate())); action->setText(i18nc("@action:inmenu", "Create software translation project...")); action = proj->addAction(QStringLiteral("project_create_odf"), Project::instance(), SLOT(projectOdfCreate())); action->setText(i18nc("@action:inmenu", "Create OpenDocument translation project...")); action = proj->addAction(QStringLiteral("project_open"), this, SLOT(openProject())); action->setText(i18nc("@action:inmenu", "Open project...")); action->setIcon(QIcon::fromTheme("project-open")); m_openRecentProjectAction = new KRecentFilesAction(i18nc("@action:inmenu", "Open recent project"), this); action = proj->addAction(QStringLiteral("project_open_recent"), m_openRecentProjectAction); connect(m_openRecentProjectAction, QOverload::of(&KRecentFilesAction::urlSelected), this, QOverload::of(&LokalizeMainWindow::openProject)); //Qt::QueuedConnection: defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash connect(Project::instance(), &Project::loaded, this, &LokalizeMainWindow::projectLoaded, Qt::QueuedConnection); ADD_ACTION_SHORTCUT("tools_filesearch", i18nc("@action:inmenu", "Search and replace in files"), Qt::Key_F6) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showFileSearch); ADD_ACTION_SHORTCUT("tools_filesearch_next", i18nc("@action:inmenu", "Find next in files"), Qt::META + Qt::Key_F3) connect(action, &QAction::triggered, this, &LokalizeMainWindow::fileSearchNext); action = ac->addAction(QStringLiteral("tools_widgettextcapture"), this, SLOT(widgetTextCapture())); action->setText(i18nc("@action:inmenu", "Widget text capture")); setupGUI(Default, QStringLiteral("lokalizemainwindowui.rc")); //qCDebug(LOKALIZE_LOG)<<"action setup finished"<subWindowList()) { if (subwindow == m_translationMemorySubWindow && m_translationMemorySubWindow) subwindow->deleteLater(); else if (qobject_cast(subwindow->widget())) { m_fileToEditor.remove(static_cast(subwindow->widget())->currentFilePath());//safety m_mdiArea->removeSubWindow(subwindow); subwindow->deleteLater(); } } Project::instance()->load(QString()); //TODO scripts return true; } void LokalizeMainWindow::openProject(QString path) { path = SettingsController::instance()->projectOpen(path, false); //dry run if (path.isEmpty()) return; if (closeProject()) SettingsController::instance()->projectOpen(path, true);//really open } void LokalizeMainWindow::saveProperties(KConfigGroup& stateGroup) { saveProjectState(stateGroup); } void LokalizeMainWindow::saveProjectState(KConfigGroup& stateGroup) { QList editors = m_mdiArea->subWindowList(); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); int activeSWIndex = -1; int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!editors.at(i) || !qobject_cast(editors.at(i)->widget())) continue; EditorState state = static_cast(editors.at(i)->widget())->state(); if (editors.at(i) == activeSW) { activeSWIndex = files.size(); m_lastEditorState = state.dockWidgets.toBase64(); } files.append(state.filePath); mergeFiles.append(state.mergeFilePath); dockWidgets.append(state.dockWidgets.toBase64()); entries.append(state.entry); //offsets.append(state.offset); //qCWarning(LOKALIZE_LOG)<(editors.at(i)->widget() )->state().url; } //if (activeSWIndex==-1 && activeSW==m_projectSubWindow) if (files.size() == 0 && !m_lastEditorState.isEmpty()) { dockWidgets.append(m_lastEditorState); // save last state if no editor open } if (stateGroup.isValid()) stateGroup.writeEntry("Project", Project::instance()->path()); KConfig config; KConfigGroup projectStateGroup(&config, "State-" + Project::instance()->path()); projectStateGroup.writeEntry("Active", activeSWIndex); projectStateGroup.writeEntry("Files", files); projectStateGroup.writeEntry("MergeFiles", mergeFiles); projectStateGroup.writeEntry("DockWidgets", dockWidgets); //stateGroup.writeEntry("Offsets",offsets); projectStateGroup.writeEntry("Entries", entries); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); if (w->unitsCount() > 0) projectStateGroup.writeEntry("UnitsCount", w->unitsCount()); } QString nameSpecifier = Project::instance()->path(); if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); KConfig* c = stateGroup.isValid() ? stateGroup.config() : &config; m_openRecentFileAction->saveEntries(KConfigGroup(c, "RecentFiles" + nameSpecifier)); m_openRecentProjectAction->saveEntries(KConfigGroup(&config, "RecentProjects")); } void LokalizeMainWindow::readProperties(const KConfigGroup& stateGroup) { KConfig config; const KConfig* c = stateGroup.isValid() ? stateGroup.config() : &config; m_openRecentProjectAction->loadEntries(KConfigGroup(c, "RecentProjects")); QString path = stateGroup.readEntry("Project", QString()); if (Project::instance()->isLoaded() || path.isEmpty()) { //1. we weren't existing yet when the signal was emitted //2. defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash QTimer::singleShot(0, this, &LokalizeMainWindow::projectLoaded); } else Project::instance()->load(path); } void LokalizeMainWindow::projectLoaded() { QString projectPath = Project::instance()->path(); qCDebug(LOKALIZE_LOG) << projectPath; m_openRecentProjectAction->addUrl(QUrl::fromLocalFile(projectPath)); KConfig config; QString nameSpecifier = projectPath; if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); m_openRecentFileAction->loadEntries(KConfigGroup(&config, "RecentFiles" + nameSpecifier)); //if project isn't loaded, still restore opened files from "State-" KConfigGroup stateGroup(&config, "State"); KConfigGroup projectStateGroup(&config, "State-" + projectPath); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; projectOverview(); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); w->setLegacyUnitsCount(projectStateGroup.readEntry("UnitsCount", 0)); QTabBar* tw = m_mdiArea->findChild(); if (tw) for (int i = 0; i < tw->count(); i++) if (tw->tabText(i) == w->windowTitle()) tw->setTabToolTip(i, Project::instance()->path()); } entries = projectStateGroup.readEntry("Entries", entries); if (Settings::self()->restoreRecentFilesOnStartup()) files = projectStateGroup.readEntry("Files", files); mergeFiles = projectStateGroup.readEntry("MergeFiles", mergeFiles); dockWidgets = projectStateGroup.readEntry("DockWidgets", dockWidgets); int i = files.size(); int activeSWIndex = projectStateGroup.readEntry("Active", -1); QStringList failedFiles; while (--i >= 0) { if (i < dockWidgets.size()) { m_lastEditorState = dockWidgets.at(i); } if (!fileOpen(files.at(i), entries.at(i)/*, offsets.at(i)*//*,&activeSW11*/, activeSWIndex == i, mergeFiles.at(i),/*silent*/true)) failedFiles.append(files.at(i)); } if (!failedFiles.isEmpty()) { qCDebug(LOKALIZE_LOG) << "failedFiles" << failedFiles; // KMessageBox::error(this, i18nc("@info","Error opening the following files:")+ // "
  • "+failedFiles.join("
  • ")+"
  • " ); KNotification* notification = new KNotification("FilesOpenError", this); notification->setText(i18nc("@info", "Error opening the following files:\n\n") % "" % failedFiles.join("
    ") % ""); notification->sendEvent(); } if (files.isEmpty() && dockWidgets.size() > 0) { m_lastEditorState = dockWidgets.last(); // restore last state if no editor open } else { m_lastEditorState = stateGroup.readEntry("DefaultDockWidgets", m_lastEditorState); // restore default state if no last editor for this project } if (activeSWIndex == -1) { m_toBeActiveSubWindow = m_projectSubWindow; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } projectSettingsChanged(); QTimer::singleShot(0, this, &LokalizeMainWindow::loadProjectScripts); } void LokalizeMainWindow::projectSettingsChanged() { //TODO show langs setCaption(Project::instance()->projectID()); } void LokalizeMainWindow::widgetTextCapture() { WidgetTextCaptureConfig* w = new WidgetTextCaptureConfig(this); w->show(); } //BEGIN DBus interface //#include "plugin.h" #include "mainwindowadaptor.h" #include #include using namespace Kross; class MyScriptingPlugin: public Kross::ScriptingPlugin { public: MyScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(lokalize) { addObject(lokalize, "Lokalize"); addObject(Project::instance(), "Project"); addObject(editor, "Editor"); setXMLFile("scriptsui.rc", true); } ~MyScriptingPlugin() {} }; #define PROJECTRCFILE "scripts.rc" #define PROJECTRCFILEDIR Project::instance()->projectDir()+"/lokalize-scripts" #define PROJECTRCFILEPATH Project::instance()->projectDir()+"/lokalize-scripts" "/" PROJECTRCFILE //TODO be lazy creating scripts dir ProjectScriptingPlugin::ProjectScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(Project::instance()->kind(), PROJECTRCFILEPATH, Project::instance()->kind(), lokalize) { if (Project::instance()->projectDir().isEmpty()) return; QString filepath = PROJECTRCFILEPATH; // Remove directory "scripts.rc" if it is empty. It could be // mistakenly created by Lokalize 15.04.x. if (QFileInfo(filepath).isDir() && !QDir().rmdir(filepath)) { qCCritical(LOKALIZE_LOG) << "Failed to remove directory" << filepath << "to create scripting configuration file with at the same path. " << "The directory may be not empty."; return; } if (!QFile::exists(filepath)) { QDir().mkdir(QFileInfo(filepath).dir().path()); QFile f(filepath); if (!f.open(QIODevice::WriteOnly)) return; QTextStream out(&f); out << ""; f.close(); } //qCWarning(LOKALIZE_LOG)<collection(Project::instance()->kind()); if (!collection) return; foreach (const QString &collectionname, collection->collections()) { Kross::ActionCollection* c = collection->collection(collectionname); if (!c->isEnabled()) continue; foreach (Kross::Action* action, c->actions()) { if (action->property("autorun").toBool()) action->trigger(); if (action->property("first-run").toBool() && Project::local()->firstRun()) action->trigger(); } } } ProjectScriptingPlugin::~ProjectScriptingPlugin() { Kross::ActionCollection* collection = Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()); if (!collection) return; QString scriptsrc = PROJECTRCFILE; QDir rcdir(PROJECTRCFILEDIR); qCDebug(LOKALIZE_LOG) << rcdir.entryList(QStringList("*.rc"), QDir::Files); foreach (const QString& rc, QDir(PROJECTRCFILEDIR).entryList(QStringList("*.rc"), QDir::Files)) if (rc != scriptsrc) qCWarning(LOKALIZE_LOG) << rc << collection->readXmlFile(rcdir.absoluteFilePath(rc)); } /* void LokalizeMainWindow::checkForProjectAlreadyOpened() { QStringList services=QDBusConnection::sessionBus().interface()->registeredServiceNames().value(); int i=services.size(); while(--i>=0) { if (services.at(i).startsWith("org.kde.lokalize")) //QDBusReply QDBusConnectionInterface::servicePid ( const QString & serviceName ) const; QDBusConnection::callWithCallback(QDBusMessage::createMethodCall(services.at(i),"/ThisIsWhatYouWant","org.kde.Lokalize.MainWindow","currentProject"), this, SLOT(), const char * errorMethod); } } */ void LokalizeMainWindow::registerDBusAdaptor() { new MainWindowAdaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String("/ThisIsWhatYouWant"), this); //qCWarning(LOKALIZE_LOG)<registeredServiceNames().value(); #ifndef Q_OS_MAC //TODO really fix!!! guiFactory()->addClient(new MyScriptingPlugin(this, m_multiEditorAdaptor)); #endif //QMenu* projectActions=static_cast(factory()->container("project_actions",this)); /* KActionCollection* actionCollection = mWindow->actionCollection(); actionCollection->action("file_save")->setEnabled(canSave); actionCollection->action("file_save_as")->setEnabled(canSave); */ } void LokalizeMainWindow::loadProjectScripts() { if (m_projectScriptingPlugin) { guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; } //a HACK to get new .rc files shown w/o requiring a restart m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); //guiFactory()->addClient(m_projectScriptingPlugin); //guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); guiFactory()->addClient(m_projectScriptingPlugin); } int LokalizeMainWindow::lookupInTranslationMemory(DocPosition::Part part, const QString& text) { TM::TMTab* w = showTM(); if (!text.isEmpty()) w->lookup(part == DocPosition::Source ? text : QString(), part == DocPosition::Target ? text : QString()); return w->dbusId(); } int LokalizeMainWindow::lookupInTranslationMemory(const QString& source, const QString& target) { TM::TMTab* w = showTM(); w->lookup(source, target); return w->dbusId(); } int LokalizeMainWindow::showTranslationMemory() { /*activateWindow(); raise(); show();*/ return lookupInTranslationMemory(DocPosition::UndefPart, QString()); } int LokalizeMainWindow::openFileInEditorAt(const QString& path, const QString& source, const QString& ctxt) { EditorTab* w = fileOpen(path, source, ctxt, true); if (!w) return -1; return w->dbusId(); } int LokalizeMainWindow::openFileInEditor(const QString& path) { return openFileInEditorAt(path, QString(), QString()); } QObject* LokalizeMainWindow::activeEditor() { //QList editors=m_mdiArea->subWindowList(); QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (!activeSW || !qobject_cast(activeSW->widget())) return 0; return activeSW->widget(); } QObject* LokalizeMainWindow::editorForFile(const QString& path) { FileToEditor::const_iterator it = m_fileToEditor.constFind(QFileInfo(path).canonicalFilePath()); if (it == m_fileToEditor.constEnd()) return 0; QMdiSubWindow* w = it.value(); if (!w) return 0; return static_cast(w->widget()); } int LokalizeMainWindow::editorIndexForFile(const QString& path) { EditorTab* editor = static_cast(editorForFile(path)); if (!editor) return -1; return editor->dbusId(); } QString LokalizeMainWindow::currentProject() { return Project::instance()->path(); } #ifdef Q_OS_WIN #include int LokalizeMainWindow::pid() { return GetCurrentProcessId(); } #else #include int LokalizeMainWindow::pid() { return getpid(); } #endif QString LokalizeMainWindow::dbusName() { return QStringLiteral("org.kde.lokalize-%1").arg(pid()); } void LokalizeMainWindow::busyCursor(bool busy) { busy ? QApplication::setOverrideCursor(Qt::WaitCursor) : QApplication::restoreOverrideCursor(); } void LokalizeMdiArea::activateNextSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activateNextSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } void LokalizeMdiArea::activatePreviousSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activatePreviousSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } MultiEditorAdaptor::MultiEditorAdaptor(EditorTab *parent) : EditorAdaptor(parent) { setObjectName(QStringLiteral("MultiEditorAdaptor")); connect(parent, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); } void MultiEditorAdaptor::setEditorTab(EditorTab* e) { if (parent()) disconnect(parent(), QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); if (e) connect(e, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); setParent(e); setAutoRelaySignals(false); setAutoRelaySignals(true); } void MultiEditorAdaptor::handleParentDestroy(QObject* p) { Q_UNUSED(p); setParent(0); } //END DBus interface DelayedFileOpener::DelayedFileOpener(const QVector& urls, LokalizeMainWindow* lmw) : QObject() , m_urls(urls) , m_lmw(lmw) { //do the work just after project loading is finished //(i.e. all the files from previous project session are loaded) QTimer::singleShot(1, this, &DelayedFileOpener::doOpen); } void DelayedFileOpener::doOpen() { int lastIndex = m_urls.count() - 1; for (int i = 0; i <= lastIndex; i++) m_lmw->fileOpen(m_urls.at(i), 0, /*set as active*/i == lastIndex); deleteLater(); } #include "moc_lokalizesubwindowbase.cpp" #include "moc_multieditoradaptor.cpp" diff --git a/src/lokalizemainwindow.h b/src/lokalizemainwindow.h index e1920e5..0db05cf 100644 --- a/src/lokalizemainwindow.h +++ b/src/lokalizemainwindow.h @@ -1,224 +1,225 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-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 . **************************************************************************** */ #ifndef LOKALIZEMAINWINDOW_H #define LOKALIZEMAINWINDOW_H #include "pos.h" #include #include #include #include #include #include #include class QLabel; class QMdiSubWindow; class QMdiArea; class QActionGroup; class LokalizeMdiArea; class KRecentFilesAction; class EditorTab; class MultiEditorAdaptor; class ProjectScriptingPlugin; class FileSearchTab; namespace TM { class TMTab; } /** * @short Lokalize MDI (tabbed) window. * * Sets up actions, and maintains their connection with active subwindow via ActionProxy * As such, it handles the menus, toolbars, and status bars. * * It is known as Lokalize in kross scripts and as * '/ThisIsWhatYouWant : org.kde.Lokalize.MainWindow' in qdbusviewer * * @author Nick Shaforostoff */ class LokalizeMainWindow: public KXmlGuiWindow { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.MainWindow") //qdbuscpp2xml -m -s lokalizemainwindow.h -o org.kde.lokalize.MainWindow.xml public: LokalizeMainWindow(); ~LokalizeMainWindow() override; protected: void saveProjectState(KConfigGroup&); void saveProperties(KConfigGroup& stateGroup) override; bool queryClose() override; void readProperties(const KConfigGroup& stateGroup) override; void registerDBusAdaptor(); void setupActions(); private slots: void slotSubWindowActivated(QMdiSubWindow*); void initLater(); void applyToBeActiveSubWindow(); void projectLoaded(); void projectSettingsChanged(); void loadProjectScripts(); void editorClosed(QObject* obj); void resetMultiEditorAdaptor(); void openProject(const QUrl& url) { openProject(url.toLocalFile()); //convenience overload for recent projects action } void openProject() { openProject(QString()); } public slots: /** * Adds new editor with @param path loaded, * or just activates already existing editor with this file. */ Q_SCRIPTABLE int openFileInEditor(const QString& path); Q_SCRIPTABLE int openFileInEditorAt(const QString& path, const QString& source, const QString& ctxt); int lookupInTranslationMemory(DocPosition::Part part, const QString& text); Q_SCRIPTABLE int lookupInTranslationMemory(const QString& source, const QString& target); Q_SCRIPTABLE int showTranslationMemory(); Q_SCRIPTABLE void showProjectOverview(); Q_SCRIPTABLE QObject* projectOverview(); Q_SCRIPTABLE bool closeProject(); Q_SCRIPTABLE void openProject(QString path); Q_SCRIPTABLE QString currentProject(); /// @returns 0 if current tab is not of Editor type Q_SCRIPTABLE QObject* activeEditor(); /// @returns editor with @param path loaded or 0 if there is no such editor. Q_SCRIPTABLE QObject* editorForFile(const QString& path); /** * # part of editor DBus path: /ThisIsWhatYouWant/Editor/# * @returns -1 if there is no such editor */ Q_SCRIPTABLE int editorIndexForFile(const QString& path); /// @returns Unix process ID Q_SCRIPTABLE int pid(); /// @returns smth like 'org.kde.lokalize-####' where #### is pid() Q_SCRIPTABLE QString dbusName(); Q_SCRIPTABLE void busyCursor(bool busy); //Q_SCRIPTABLE void processEvents(); //returns 0 if error EditorTab* fileOpen_(QString url, const bool setAsActive); EditorTab* fileOpen(QString url = QString(), int entry = 0, bool setAsActive = true, const QString& mergeFile = QString(), bool silent = false); EditorTab* fileOpen(const QString& url, const QString& source, const QString& ctxt, const bool setAsActive); EditorTab* fileOpen(const QString& url, DocPosition docPos, int selection, const bool setAsActive); EditorTab* fileOpen(const QUrl& url) { return fileOpen(url.toLocalFile(), 0, true); } TM::TMTab* showTM(); FileSearchTab* showFileSearch(bool activate = true); void fileSearchNext(); void addFilesToSearch(const QStringList&); void widgetTextCapture(); signals: Q_SCRIPTABLE void editorAdded(); Q_SCRIPTABLE void editorActivated(); private: LokalizeMdiArea* m_mdiArea; QPointer m_prevSubWindow; QPointer m_projectSubWindow; QPointer m_translationMemorySubWindow; QPointer m_fileSearchSubWindow; QPointer m_toBeActiveSubWindow;//used during session restore QActionGroup* m_editorActions; QActionGroup* m_managerActions; KRecentFilesAction* m_openRecentFileAction; KRecentFilesAction* m_openRecentProjectAction; QVector m_statusBarLabels; QByteArray m_lastEditorState; //used for kross API EditorTab* m_spareEditor; MultiEditorAdaptor* m_multiEditorAdaptor; ProjectScriptingPlugin* m_projectScriptingPlugin; //using QPointer switches it.value() to 0 before we get to destroyed() handler //typedef QMap > FileToEditor; typedef QMap FileToEditor; FileToEditor m_fileToEditor; }; class LokalizeMdiArea: public QMdiArea { Q_OBJECT public slots: void activateNextSubWindow(); void activatePreviousSubWindow(); }; #include class ProjectScriptingPlugin: public Kross::ScriptingPlugin { Q_OBJECT public: ProjectScriptingPlugin(QObject* lokalize, QObject* editor); ~ProjectScriptingPlugin(); void setDOMDocument(const QDomDocument &document, bool merge = false) override; private slots: void doAutoruns(); }; class DelayedFileOpener: public QObject { Q_OBJECT public: DelayedFileOpener(const QVector& urls, LokalizeMainWindow* lmw); private slots: void doOpen(); private: QVector m_urls; LokalizeMainWindow* m_lmw; }; #endif diff --git a/src/lokalizesubwindowbase.h b/src/lokalizesubwindowbase.h index b12f1b3..7d2bc59 100644 --- a/src/lokalizesubwindowbase.h +++ b/src/lokalizesubwindowbase.h @@ -1,115 +1,116 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 . **************************************************************************** */ #ifndef LOKALIZESUBWINDOWBASE_H #define LOKALIZESUBWINDOWBASE_H #include #include #include #include #include #include "actionproxy.h" #include #include /** * Interface for LokalizeMainWindow */ class LokalizeSubwindowBase: public KMainWindow { Q_OBJECT public: explicit LokalizeSubwindowBase(QWidget* parent): KMainWindow(parent) {} virtual ~LokalizeSubwindowBase() { emit aboutToBeClosed(); } virtual KXMLGUIClient* guiClient() = 0; virtual void reloadUpdatedXML() = 0; virtual void setUpdatedXMLFile() = 0; //interface for LokalizeMainWindow virtual void hideDocks() = 0; virtual void showDocks() = 0; //bool queryClose(); virtual QString currentFilePath() { return QString(); } protected: void reflectNonApprovedCount(int count, int total); void reflectUntranslatedCount(int count, int total); signals: void aboutToBeClosed(); public: //QHash supportedActions; StatusBarProxy statusBarItems; protected: QDateTime lastXMLUpdate; }; /** * C++ casting workaround */ class LokalizeSubwindowBase2: public LokalizeSubwindowBase, public KXMLGUIClient { public: explicit LokalizeSubwindowBase2(QWidget* parent): LokalizeSubwindowBase(parent), KXMLGUIClient() {} ~LokalizeSubwindowBase2() override {} KXMLGUIClient* guiClient() override { return (KXMLGUIClient*)this; } void setUpdatedXMLFile() override { QString localXml = guiClient()->localXMLFile(); if (QFile::exists(localXml)) { lastXMLUpdate = QFileInfo(localXml).lastModified(); } } void reloadUpdatedXML() override { QString localXml = guiClient()->localXMLFile(); if (QFile::exists(localXml)) { QDateTime newXMLUpdate = QFileInfo(localXml).lastModified(); if (newXMLUpdate > lastXMLUpdate) { lastXMLUpdate = newXMLUpdate; guiClient()->reloadXML(); } } } }; #endif diff --git a/src/main.cpp b/src/main.cpp index d995d73..ca4e92c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,164 +1,165 @@ /* **************************************************************************** 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 "lokalize_debug.h" #include "project.h" #include "prefs.h" #include "prefs_lokalize.h" #include "version.h" #include "projecttab.h" #include "projectmodel.h" #include "lokalizemainwindow.h" #include "stemming.h" #include "jobs.h" #include "catalogstring.h" #include "pos.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "editortab.h" int main(int argc, char **argv) { TM::threadPool()->setMaxThreadCount(1); TM::threadPool()->setExpiryTimeout(-1); QThreadPool::globalInstance()->setMaxThreadCount(1); QApplication app(argc, argv); KCrash::initialize(); KLocalizedString::setApplicationDomain("lokalize"); QCommandLineParser parser; KAboutData about(QStringLiteral("lokalize"), i18nc("@title", "Lokalize"), LOKALIZE_VERSION, i18n("Computer-aided translation system.\nDo not translate what had already been translated."), - KAboutLicense::GPL, i18nc("@info:credit", "(c) 2007-2015 Nick Shaforostoff\n(c) 1999-2006 The KBabel developers") /*, KLocalizedString(), 0, "shafff@ukr.net"*/); + KAboutLicense::GPL, i18nc("@info:credit", "(c) 2018-2019 Simon Depiets\n(c) 2007-2015 Nick Shaforostoff\n(c) 1999-2006 The KBabel developers") /*, KLocalizedString(), 0, "shafff@ukr.net"*/); about.addAuthor(i18n("Nick Shaforostoff"), QString(), QStringLiteral("shaforostoff@gmail.com")); about.addCredit(i18n("Google Inc."), i18n("sponsored development as part of Google Summer Of Code program"), QString(), QStringLiteral("http://google.com")); about.addCredit(i18n("NLNet Foundation"), i18n("sponsored XLIFF-related work"), QString(), QStringLiteral("https://nlnet.nl/")); about.addCredit(i18n("Translate-toolkit"), i18n("provided excellent cross-format converting scripts"), QString(), QStringLiteral("http://translate.sourceforge.net")); about.addCredit(i18n("Viesturs Zarins"), i18n("project tree merging translation+templates"), QStringLiteral("viesturs.zarins@mii.lu.lv"), QString()); about.addCredit(i18n("Stephan Johach"), i18n("bug fixing patches"), QStringLiteral("hunsum@gmx.de")); about.addCredit(i18n("Chusslove Illich"), i18n("bug fixing patches"), QStringLiteral("caslav.ilic@gmx.net")); about.addCredit(i18n("Jure Repinc"), i18n("testing and bug fixing"), QStringLiteral("jlp@holodeck1.com")); about.addCredit(i18n("Stefan Asserhall"), i18n("patches"), QStringLiteral("stefan.asserhall@comhem.se")); about.addCredit(i18n("Papp Laszlo"), i18n("bug fixing patches"), QStringLiteral("djszapi@archlinux.us")); about.addCredit(i18n("Albert Astals Cid"), i18n("XLIFF improvements"), QStringLiteral("aacid@kde.org")); about.addCredit(i18n("Simon Depiets"), i18n("bug fixing and improvements"), QStringLiteral("sdepiets@gmail.com")); KAboutData::setApplicationData(about); about.setupCommandLine(&parser); //parser.addOption(QCommandLineOption(QStringList() << QLatin1String("source"), i18n( "Source for the merge mode" ), QLatin1String("URL"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("noprojectscan"), i18n("Do not scan files of the project."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("project"), i18n("Load specified project."), QStringLiteral("filename"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+[URL]"), i18n("Document to open"))); parser.process(app); about.processCommandLine(&parser); //qCDebug(LOKALIZE_LOG) is important as it avoids compile 'optimization'. qCDebug(LOKALIZE_LOG) << qRegisterMetaType(); qCDebug(LOKALIZE_LOG) << qRegisterMetaType(); qCDebug(LOKALIZE_LOG) << qRegisterMetaType(); qCDebug(LOKALIZE_LOG) << qRegisterMetaType(); qRegisterMetaTypeStreamOperators("InlineTag"); qRegisterMetaTypeStreamOperators("CatalogString"); qAddPostRoutine(&cleanupSpellers); const KDBusService dbusService(KDBusService::Multiple); // see if we are starting with session management if (app.isSessionRestored()) kRestoreMainWindows(); else { // no session.. just start up normally QString projectFilePath = parser.value(QStringLiteral("project")); QVector urls; Q_FOREACH (const QString& filePath, parser.positionalArguments()) if (filePath.endsWith(QLatin1String(".lokalize"))) projectFilePath = filePath; else if (QFileInfo::exists(filePath)) urls.append(filePath); if (projectFilePath.length()) { // load needs an absolute path // FIXME: I do not know how to handle urls here // bug 245546 regarding symlinks QFileInfo projectFileInfo(projectFilePath); projectFilePath = projectFileInfo.canonicalFilePath(); if (projectFilePath.isEmpty()) projectFilePath = projectFileInfo.absoluteFilePath(); Project::instance()->load(projectFilePath); } LokalizeMainWindow* lmw = new LokalizeMainWindow; SettingsController::instance()->setMainWindowPtr(lmw); lmw->show(); if (urls.size()) new DelayedFileOpener(urls, lmw); //Project::instance()->model()->setCompleteScan(parser.isSet("noprojectscan"));// TODO: negate check (and ensure nobody passes the no-op --noprojectscan argument) } int code = app.exec(); QThreadPool::globalInstance()->clear(); TM::cancelAllJobs(); TM::threadPool()->clear(); TM::threadPool()->waitForDone(1000); Project::instance()->model()->threadPool()->clear(); if (SettingsController::instance()->dirty) //for config changes done w/o config dialog Settings::self()->save(); if (Project::instance()->isLoaded()) Project::instance()->save(); qCDebug(LOKALIZE_LOG) << "Finishing Project jobs..."; qCDebug(LOKALIZE_LOG) << "Finishing TM jobs..."; int secs = 5; while (--secs >= 0) { Project::instance()->model()->threadPool()->waitForDone(1000); TM::threadPool()->waitForDone(1000); QThreadPool::globalInstance()->waitForDone(1000); //qCDebug(LOKALIZE_LOG)<<"QCoreApplication::processEvents()..."; QCoreApplication::processEvents(); QCoreApplication::sendPostedEvents(0, 0); } return code; } diff --git a/src/mergemode/mergecatalog.cpp b/src/mergemode/mergecatalog.cpp index 45c23e7..e723fa3 100644 --- a/src/mergemode/mergecatalog.cpp +++ b/src/mergemode/mergecatalog.cpp @@ -1,333 +1,334 @@ /* **************************************************************************** This file is part of Lokalize - Copyright (C) 2007-2014 by Nick Shaforostoff + 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 "mergecatalog.h" #include "lokalize_debug.h" #include "catalog_private.h" #include "catalogstorage.h" #include "cmd.h" #include #include #include MergeCatalog::MergeCatalog(QObject* parent, Catalog* baseCatalog, bool saveChanges) : Catalog(parent) , m_baseCatalog(baseCatalog) , m_unmatchedCount(0) , m_modified(false) { setActivePhase(baseCatalog->activePhase(), baseCatalog->activePhaseRole()); if (saveChanges) { connect(baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); connect(baseCatalog, QOverload<>::of(&Catalog::signalFileSaved), this, &MergeCatalog::save); } } void MergeCatalog::copyFromBaseCatalog(const DocPosition& pos, int options) { bool a = m_mergeDiffIndex.contains(pos.entry); if (options & EvenIfNotInDiffIndex || !a) { //sync changes DocPosition ourPos = pos; if ((ourPos.entry = m_map.at(ourPos.entry)) == -1) return; //note the explicit use of map... if (m_storage->isApproved(ourPos) != m_baseCatalog->isApproved(pos)) //qCWarning(LOKALIZE_LOG)<setApproved(ourPos, m_baseCatalog->isApproved(pos)); DocPos p(pos); if (!m_originalHashes.contains(p)) m_originalHashes[p] = qHash(m_storage->target(ourPos)); m_storage->setTarget(ourPos, m_baseCatalog->target(pos)); setModified(ourPos, true); if (options & EvenIfNotInDiffIndex && a) m_mergeDiffIndex.removeAll(pos.entry); m_modified = true; emit signalEntryModified(pos); } } QString MergeCatalog::msgstr(const DocPosition& pos) const { DocPosition us = pos; us.entry = m_map.at(pos.entry); return (us.entry == -1) ? QString() : Catalog::msgstr(us); } bool MergeCatalog::isApproved(uint index) const { return (m_map.at(index) == -1) ? false : Catalog::isApproved(m_map.at(index)); } TargetState MergeCatalog::state(const DocPosition& pos) const { DocPosition us = pos; us.entry = m_map.at(pos.entry); return (us.entry == -1) ? New : Catalog::state(us); } bool MergeCatalog::isPlural(uint index) const { //sanity if (m_map.at(index) == -1) { qCWarning(LOKALIZE_LOG) << "!!! index" << index << "m_map.at(index)" << m_map.at(index) << "numberOfEntries()" << numberOfEntries(); return false; } return Catalog::isPlural(m_map.at(index)); } bool MergeCatalog::isPresent(const int& entry) const { return m_map.at(entry) != -1; } MatchItem MergeCatalog::calcMatchItem(const DocPosition& basePos, const DocPosition& mergePos) { CatalogStorage& baseStorage = *(m_baseCatalog->m_storage); CatalogStorage& mergeStorage = *(m_storage); MatchItem item(mergePos.entry, basePos.entry, true); //TODO make more robust, perhaps after XLIFF? QStringList baseMatchData = baseStorage.matchData(basePos); QStringList mergeMatchData = mergeStorage.matchData(mergePos); //compare ids item.score += 40 * ((baseMatchData.isEmpty() && mergeMatchData.isEmpty()) ? baseStorage.id(basePos) == mergeStorage.id(mergePos) : baseMatchData == mergeMatchData); //TODO look also for changed/new s //translation isn't changed if (baseStorage.targetAllForms(basePos, true) == mergeStorage.targetAllForms(mergePos, true)) { item.translationIsDifferent = baseStorage.isApproved(basePos) != mergeStorage.isApproved(mergePos); item.score += 29 + 1 * item.translationIsDifferent; } #if 0 if (baseStorage.source(basePos) == "%1 (%2)") { qCDebug(LOKALIZE_LOG) << "BASE"; qCDebug(LOKALIZE_LOG) << m_baseCatalog->url(); qCDebug(LOKALIZE_LOG) << basePos.entry; qCDebug(LOKALIZE_LOG) << baseStorage.source(basePos); qCDebug(LOKALIZE_LOG) << baseMatchData.first(); qCDebug(LOKALIZE_LOG) << "MERGE"; qCDebug(LOKALIZE_LOG) << url(); qCDebug(LOKALIZE_LOG) << mergePos.entry; qCDebug(LOKALIZE_LOG) << mergeStorage.source(mergePos); qCDebug(LOKALIZE_LOG) << mergeStorage.matchData(mergePos).first(); qCDebug(LOKALIZE_LOG) << item.score; qCDebug(LOKALIZE_LOG) << ""; } #endif return item; } static QString strip(QString source) { source.remove('\n'); return source; } int MergeCatalog::loadFromUrl(const QString& filePath) { int errorLine = Catalog::loadFromUrl(filePath); if (Q_UNLIKELY(errorLine != 0)) return errorLine; //now calc the entry mapping CatalogStorage& baseStorage = *(m_baseCatalog->m_storage); CatalogStorage& mergeStorage = *(m_storage); DocPosition i(0); int size = baseStorage.size(); int mergeSize = mergeStorage.size(); m_map.fill(-1, size); QMultiMap backMap; //will be used to maintain one-to-one relation //precalc for fast lookup QMultiHash mergeMap; while (i.entry < mergeSize) { mergeMap.insert(strip(mergeStorage.source(i)), i.entry); ++(i.entry); } i.entry = 0; while (i.entry < size) { QString key = strip(baseStorage.source(i)); const QList& entries = mergeMap.values(key); QList scores; int k = entries.size(); if (k) { while (--k >= 0) scores << calcMatchItem(i, DocPosition(entries.at(k))); qSort(scores.begin(), scores.end(), qGreater()); m_map[i.entry] = scores.first().mergeEntry; backMap.insert(scores.first().mergeEntry, i.entry); if (scores.first().translationIsDifferent) m_mergeDiffIndex.append(i.entry); } ++(i.entry); } //maintain one-to-one relation const QList& mergePositions = backMap.uniqueKeys(); foreach (int mergePosition, mergePositions) { const QList& basePositions = backMap.values(mergePosition); if (basePositions.size() == 1) continue; //qCDebug(LOKALIZE_LOG)<<"kv"< scores; foreach (int value, basePositions) scores << calcMatchItem(DocPosition(value), mergePosition); qSort(scores.begin(), scores.end(), qGreater()); int i = scores.size(); while (--i > 0) { //qCDebug(LOKALIZE_LOG)<<"erasing"<::iterator it = mergeMap.begin(); while (it != mergeMap.end()) { //qCWarning(LOKALIZE_LOG)<msgstr(pos) != msgstr(pos); m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept change in translation")); if (m_baseCatalog->state(pos) != state(pos)) SetStateCmd::instantiateAndPush(m_baseCatalog, pos, state(pos)); if (changeContents) { pos.offset = 0; if (!m_baseCatalog->msgstr(pos).isEmpty()) m_baseCatalog->push(new DelTextCmd(m_baseCatalog, pos, m_baseCatalog->msgstr(pos))); m_baseCatalog->push(new InsTextCmd(m_baseCatalog, pos, msgstr(pos))); } ////////this is NOT done automatically by BaseCatalogEntryChanged slot bool remove = true; if (isPlural(pos.entry)) { DocPosition p = pos; p.form = qMin(m_baseCatalog->numberOfPluralForms(), numberOfPluralForms()); //just sanity check p.form = qMax((int)p.form, 1); //just sanity check while ((--(p.form)) >= 0 && remove) remove = m_baseCatalog->msgstr(p) == msgstr(p); } if (remove) removeFromDiffIndex(pos.entry); m_baseCatalog->endMacro(); } void MergeCatalog::copyToBaseCatalog(int options) { DocPosition pos; pos.offset = 0; bool insHappened = false; QLinkedList changed = differentEntries(); foreach (int entry, changed) { pos.entry = entry; if (options & EmptyOnly && !m_baseCatalog->isEmpty(entry)) continue; if (options & HigherOnly && !m_baseCatalog->isEmpty(entry) && m_baseCatalog->state(pos) >= state(pos)) continue; int formsCount = (m_baseCatalog->isPlural(entry)) ? m_baseCatalog->numberOfPluralForms() : 1; pos.form = 0; while (pos.form < formsCount) { //m_baseCatalog->push(new DelTextCmd(m_baseCatalog,pos,m_baseCatalog->msgstr(pos.entry,0))); ? //some forms may still contain translation... if (!(options & EmptyOnly && !m_baseCatalog->isEmpty(pos)) /*&& !(options&HigherOnly && !m_baseCatalog->isEmpty(pos) && m_baseCatalog->state(pos)>=state(pos))*/) { if (!insHappened) { //stop basecatalog from sending signalEntryModified to us //when we are the ones who does the modification disconnect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); insHappened = true; m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept all new translations")); } copyToBaseCatalog(pos); /// /// /// m_baseCatalog->push(new InsTextCmd(m_baseCatalog,pos,mergeCatalog.msgstr(pos))); /// /// } ++(pos.form); } /// /// /// removeFromDiffIndex(m_pos.entry); /// /// } if (insHappened) { m_baseCatalog->endMacro(); //reconnect to catch all modifications coming from outside connect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); } } diff --git a/src/mergemode/mergecatalog.h b/src/mergemode/mergecatalog.h index fc0a4b1..6ba7862 100644 --- a/src/mergemode/mergecatalog.h +++ b/src/mergemode/mergecatalog.h @@ -1,165 +1,166 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef MERGECATALOG_H #define MERGECATALOG_H #include "catalog.h" #include #include class KAutoSaveFile; struct MatchItem { int mergeEntry: 32; int baseEntry: 32; short score: 16; short translationIsDifferent: 16; MatchItem() : mergeEntry(0) , baseEntry(0) , score(0) , translationIsDifferent(false) {} MatchItem(int m, int b, bool d) : mergeEntry(m) , baseEntry(b) , score(0) , translationIsDifferent(d) {} bool operator<(const MatchItem& other) const { return score < other.score; } }; /** * Merge source container. * Parallel editing is available. * What this subclass does is creating the map (to provide main-file entry order) and index for fast changed entry lookup * So order of entries the same as of main-file * * TODO index of ch - on-the-fly * @short Merge source container * @author Nick Shaforostoff */ class MergeCatalog: public Catalog { Q_OBJECT public: explicit MergeCatalog(QObject* parent, Catalog* baseCatalog, bool saveChanges = true); ~MergeCatalog() override {} int loadFromUrl(const QString& filePath); int firstChangedIndex() const { return m_mergeDiffIndex.isEmpty() ? numberOfEntries() : m_mergeDiffIndex.first(); } int lastChangedIndex() const { return m_mergeDiffIndex.isEmpty() ? -1 : m_mergeDiffIndex.last(); } int nextChangedIndex(uint index) const { return findNextInList(m_mergeDiffIndex, index); } int prevChangedIndex(uint index) const { return findPrevInList(m_mergeDiffIndex, index); } int isDifferent(uint index) const { return m_mergeDiffIndex.contains(index); } QLinkedList differentEntries()const { return m_mergeDiffIndex; } //override to use map QString msgstr(const DocPosition&) const override; bool isApproved(uint index) const; bool isPlural(uint index) const; TargetState state(const DocPosition& pos) const; int unmatchedCount()const { return m_unmatchedCount; } /// whether 'merge source' has entry with such msgid bool isPresent(const int& entry) const; bool isModified(DocPos)const; bool isModified()const { return m_modified; } ///@arg pos in baseCatalog's coordinates void copyToBaseCatalog(DocPosition& pos); enum CopyToBaseOptions {EmptyOnly = 1, HigherOnly = 2}; void copyToBaseCatalog(int options = 0); inline void removeFromDiffIndex(uint index) { m_mergeDiffIndex.removeAll(index); } enum CopyFromBaseOptions {EvenIfNotInDiffIndex = 1}; void copyFromBaseCatalog(const DocPosition&, int options); void copyFromBaseCatalog(const DocPosition& pos) { copyFromBaseCatalog(pos, EvenIfNotInDiffIndex); } public slots: void copyFromBaseCatalogIfInDiffIndex(const DocPosition& pos) { copyFromBaseCatalog(pos, 0); } bool save(); //reimplement to do save only when changes were actually done to this catalog private: MatchItem calcMatchItem(const DocPosition& basePos, const DocPosition& mergePos); KAutoSaveFile* checkAutoSave(const QString&) override { return nullptr; //rely on basecatalog restore } private: QVector m_map; //maps entries: m_baseCatalog -> this Catalog* m_baseCatalog; QLinkedList m_mergeDiffIndex;//points to baseCatalog entries QMap m_originalHashes; //for modified units only int m_unmatchedCount; bool m_modified; //need own var here cause we don't use qundostack system for merging }; #endif diff --git a/src/mergemode/mergeview.cpp b/src/mergemode/mergeview.cpp index 81be5df..3e60bf7 100644 --- a/src/mergemode/mergeview.cpp +++ b/src/mergemode/mergeview.cpp @@ -1,371 +1,372 @@ /* **************************************************************************** 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 "mergeview.h" #include "cmd.h" #include "mergecatalog.h" #include "project.h" #include "diff.h" #include #include #include #include #include #include #include #include #include #include MergeView::MergeView(QWidget* parent, Catalog* catalog, bool primary) : QDockWidget(primary ? i18nc("@title:window that displays difference between current file and 'merge source'", "Primary Sync") : i18nc("@title:window that displays difference between current file and 'merge source'", "Secondary Sync"), parent) , m_browser(new QTextEdit(this)) , m_baseCatalog(catalog) , m_mergeCatalog(0) , m_normTitle(primary ? i18nc("@title:window that displays difference between current file and 'merge source'", "Primary Sync") : i18nc("@title:window that displays difference between current file and 'merge source'", "Secondary Sync")) , m_hasInfoTitle(m_normTitle + " [*]") , m_hasInfo(false) , m_primary(primary) { setObjectName(primary ? QStringLiteral("mergeView-primary") : QStringLiteral("mergeView-secondary")); setWidget(m_browser); setToolTip(i18nc("@info:tooltip", "Drop file to be merged into / synced with the current one here, then see context menu options")); hide(); setAcceptDrops(true); m_browser->setReadOnly(true); m_browser->setContextMenuPolicy(Qt::NoContextMenu); m_browser->viewport()->setBackgroundRole(QPalette::Background); setContextMenuPolicy(Qt::ActionsContextMenu); } MergeView::~MergeView() { delete m_mergeCatalog; emit mergeCatalogPointerChanged(NULL); emit mergeCatalogAvailable(false); } QString MergeView::filePath() { if (m_mergeCatalog) return m_mergeCatalog->url(); return QString(); } void MergeView::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasUrls() && Catalog::extIsSupported(event->mimeData()->urls().first().path())) event->acceptProposedAction(); } void MergeView::dropEvent(QDropEvent *event) { mergeOpen(event->mimeData()->urls().first().toLocalFile()); event->acceptProposedAction(); } void MergeView::slotUpdate(const DocPosition& pos) { if (pos.entry == m_pos.entry) slotNewEntryDisplayed(pos); } void MergeView::slotNewEntryDisplayed(const DocPosition& pos) { m_pos = pos; if (!m_mergeCatalog) return; emit signalPriorChangedAvailable((pos.entry > m_mergeCatalog->firstChangedIndex()) || (pluralFormsAvailableBackward() != -1)); emit signalNextChangedAvailable((pos.entry < m_mergeCatalog->lastChangedIndex()) || (pluralFormsAvailableForward() != -1)); if (!m_mergeCatalog->isPresent(pos.entry)) { //i.e. no corresponding entry, whether changed or not if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); m_browser->clear(); // m_browser->viewport()->setBackgroundRole(QPalette::Base); } emit signalEntryWithMergeDisplayed(false); /// no editing at all! //////////// return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } emit signalEntryWithMergeDisplayed(m_mergeCatalog->isDifferent(pos.entry)); QString result = userVisibleWordDiff(m_baseCatalog->msgstr(pos), m_mergeCatalog->msgstr(pos), Project::instance()->accel(), Project::instance()->markup(), Html); #if 0 int i = -1; bool inTag = false; while (++i < result.size()) { //dynamic if (!inTag) { if (result.at(i) == '<') inTag = true; else if (result.at(i) == ' ') result.replace(i, 1, "&sp;"); } else if (result.at(i) == '>') inTag = false; } #endif if (!m_mergeCatalog->isApproved(pos.entry)) { result.prepend(""); result.append(""); } if (m_mergeCatalog->isModified(pos)) { result.prepend(""); result.append(""); } result.replace(' ', QChar::Nbsp); m_browser->setHtml(result); //qCDebug(LOKALIZE_LOG)<<"ELA "<clear(); } void MergeView::mergeOpen(QString mergeFilePath) { if (Q_UNLIKELY(!m_baseCatalog->numberOfEntries())) return; if (mergeFilePath == m_baseCatalog->url()) { //(we are likely to be _mergeViewSecondary) //special handling: open corresponding file in the branch //for AutoSync QString path = QFileInfo(mergeFilePath).canonicalFilePath(); //bug 245546 regarding symlinks QString oldPath = path; path.replace(Project::instance()->poDir(), Project::instance()->branchDir()); if (oldPath == path) { //if file doesn't exist both are empty cleanup(); return; } mergeFilePath = path; } if (mergeFilePath.isEmpty()) { //Project::instance()->model()->weaver()->suspend(); //KDE5PORT use mutex if needed mergeFilePath = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Select translation file"), QString(), Catalog::supportedFileTypes(false)); //Project::instance()->model()->weaver()->resume(); } if (mergeFilePath.isEmpty()) return; delete m_mergeCatalog; m_mergeCatalog = new MergeCatalog(this, m_baseCatalog); emit mergeCatalogPointerChanged(m_mergeCatalog); emit mergeCatalogAvailable(m_mergeCatalog); int errorLine = m_mergeCatalog->loadFromUrl(mergeFilePath); if (Q_LIKELY(errorLine == 0)) { if (m_pos.entry > 0) emit signalPriorChangedAvailable(m_pos.entry > m_mergeCatalog->firstChangedIndex()); emit signalNextChangedAvailable(m_pos.entry < m_mergeCatalog->lastChangedIndex()); //a bit hacky :) connect(m_mergeCatalog, &MergeCatalog::signalEntryModified, this, &MergeView::slotUpdate); if (m_pos.entry != -1) slotNewEntryDisplayed(m_pos); show(); } else { //KMessageBox::error(this, KIO::NetAccess::lastErrorString() ); cleanup(); if (errorLine > 0) KMessageBox::error(this, i18nc("@info", "Error opening the file %1 for synchronization, error line: %2", mergeFilePath, errorLine)); else { /* disable this as requested by bug 272587 KNotification* notification=new KNotification("MergeFilesOpenError", this); notification->setText( i18nc("@info %1 is full filename","Error opening the file %1 for synchronization",url.pathOrUrl()) ); notification->sendEvent(); */ } //i18nc("@info %1 is w/o path","No branch counterpart for %1",url.fileName()), } } bool MergeView::isModified() { return m_mergeCatalog && m_mergeCatalog->isModified(); //not isClean because mergecatalog doesn't keep history } int MergeView::pluralFormsAvailableForward() { if (Q_LIKELY(m_pos.entry == -1 || !m_mergeCatalog->isPlural(m_pos.entry))) return -1; int formLimit = qMin(m_baseCatalog->numberOfPluralForms(), m_mergeCatalog->numberOfPluralForms()); //just sanity check DocPosition pos = m_pos; while (++(pos.form) < formLimit) { if (m_baseCatalog->msgstr(pos) != m_mergeCatalog->msgstr(pos)) return pos.form; } return -1; } int MergeView::pluralFormsAvailableBackward() { if (Q_LIKELY(m_pos.entry == -1 || !m_mergeCatalog->isPlural(m_pos.entry))) return -1; DocPosition pos = m_pos; while (--(pos.form) >= 0) { if (m_baseCatalog->msgstr(pos) != m_mergeCatalog->msgstr(pos)) return pos.form; } return -1; } void MergeView::gotoPrevChanged() { if (Q_UNLIKELY(!m_mergeCatalog)) return; DocPosition pos; //first, check if there any plural forms waiting to be synced int form = pluralFormsAvailableBackward(); if (Q_UNLIKELY(form != -1)) { pos = m_pos; pos.form = form; } else if (Q_UNLIKELY((pos.entry = m_mergeCatalog->prevChangedIndex(m_pos.entry)) == -1)) return; if (Q_UNLIKELY(m_mergeCatalog->isPlural(pos.entry) && form == -1)) pos.form = qMin(m_baseCatalog->numberOfPluralForms(), m_mergeCatalog->numberOfPluralForms()) - 1; emit gotoEntry(pos, 0); } void MergeView::gotoNextChangedApproved() { gotoNextChanged(true); } void MergeView::gotoNextChanged(bool approvedOnly) { if (Q_UNLIKELY(!m_mergeCatalog)) return; DocPosition pos = m_pos; //first, check if there any plural forms waiting to be synced int form = pluralFormsAvailableForward(); if (Q_UNLIKELY(form != -1)) { pos = m_pos; pos.form = form; } else if (Q_UNLIKELY((pos.entry = m_mergeCatalog->nextChangedIndex(m_pos.entry)) == -1)) return; while (approvedOnly && !m_mergeCatalog->isApproved(pos.entry)) { if (Q_UNLIKELY((pos.entry = m_mergeCatalog->nextChangedIndex(pos.entry)) == -1)) return; } emit gotoEntry(pos, 0); } void MergeView::mergeBack() { if (m_pos.entry == -1 || !m_mergeCatalog || m_baseCatalog->msgstr(m_pos).isEmpty()) return; m_mergeCatalog->copyFromBaseCatalog(m_pos); } void MergeView::mergeAccept() { if (m_pos.entry == -1 || !m_mergeCatalog //||m_baseCatalog->msgstr(m_pos)==m_mergeCatalog->msgstr(m_pos) || m_mergeCatalog->msgstr(m_pos).isEmpty()) return; m_mergeCatalog->copyToBaseCatalog(m_pos); emit gotoEntry(m_pos, 0); } void MergeView::mergeAcceptAllForEmpty() { if (Q_UNLIKELY(!m_mergeCatalog)) return; bool update = m_mergeCatalog->differentEntries().contains(m_pos.entry); m_mergeCatalog->copyToBaseCatalog(/*MergeCatalog::EmptyOnly*/MergeCatalog::HigherOnly); if (update != m_mergeCatalog->differentEntries().contains(m_pos.entry)) emit gotoEntry(m_pos, 0); } bool MergeView::event(QEvent *event) { if (event->type() == QEvent::ToolTip && m_mergeCatalog) { QHelpEvent *helpEvent = static_cast(event); QString text = QStringLiteral("") % QDir::toNativeSeparators(filePath()) % QStringLiteral("\n") % i18nc("@info:tooltip", "Different entries: %1\nUnmatched entries: %2", m_mergeCatalog->differentEntries().count(), m_mergeCatalog->unmatchedCount()); text.replace('\n', QStringLiteral("
    ")); QToolTip::showText(helpEvent->globalPos(), text); return true; } return QWidget::event(event); } diff --git a/src/mergemode/mergeview.h b/src/mergemode/mergeview.h index 6b57d1a..8f2252e 100644 --- a/src/mergemode/mergeview.h +++ b/src/mergemode/mergeview.h @@ -1,100 +1,101 @@ /* **************************************************************************** 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 MERGEVIEW_H #define MERGEVIEW_H #include "pos.h" #include "mergecatalog.h" #include class QTextEdit; class Catalog; class MergeCatalog; class QDragEnterEvent; class QDropEvent; class MergeView: public QDockWidget { Q_OBJECT public: explicit MergeView(QWidget*, Catalog*, bool primary); ~MergeView() override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent*) override; QString filePath(); bool isModified(); private: /** * checks if there are any other plural forms waiting to be synced for current pos * @returns number of form or -1 */ int pluralFormsAvailableForward(); int pluralFormsAvailableBackward(); bool event(QEvent *event) override; public slots: void mergeOpen(QString mergeFilePath = QString()); void cleanup(); void slotNewEntryDisplayed(const DocPosition&); void slotUpdate(const DocPosition&); void gotoNextChanged(bool approvedOnly = false); void gotoNextChangedApproved(); void gotoPrevChanged(); void mergeAccept(); void mergeAcceptAllForEmpty(); void mergeBack(); signals: // //we connect it to our internal mergeCatalog to remove entry from index // void entryModified(uint); void signalPriorChangedAvailable(bool); void signalNextChangedAvailable(bool); void signalEntryWithMergeDisplayed(bool); void gotoEntry(const DocPosition&, int); void mergeCatalogAvailable(bool); void mergeCatalogPointerChanged(MergeCatalog* mergeCatalog); private: QTextEdit* m_browser; Catalog* m_baseCatalog; MergeCatalog* m_mergeCatalog; DocPosition m_pos; QString m_normTitle; QString m_hasInfoTitle; bool m_hasInfo; bool m_primary; }; #endif diff --git a/src/msgctxtview.cpp b/src/msgctxtview.cpp index e7cea31..c1f26cc 100644 --- a/src/msgctxtview.cpp +++ b/src/msgctxtview.cpp @@ -1,331 +1,332 @@ /* **************************************************************************** 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 "msgctxtview.h" #include "noteeditor.h" #include "catalog.h" #include "cmd.h" #include "prefs_lokalize.h" #include "project.h" #include "lokalize_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include MsgCtxtView::MsgCtxtView(QWidget* parent, Catalog* catalog) : QDockWidget(i18nc("@title toolview name", "Unit metadata"), parent) , m_browser(new QTextBrowser(this)) , m_editor(0) , m_catalog(catalog) , m_selection(0) , m_offset(0) , m_hasInfo(false) , m_hasErrorNotes(false) , m_pologyProcessInProgress(0) , m_pologyStartedReceivingOutput(false) { setObjectName(QStringLiteral("msgCtxtView")); QWidget* main = new QWidget(this); setWidget(main); m_stackedLayout = new QStackedLayout(main); m_stackedLayout->addWidget(m_browser); m_browser->viewport()->setBackgroundRole(QPalette::Background); m_browser->setOpenLinks(false); connect(m_browser, &QTextBrowser::anchorClicked, this, &MsgCtxtView::anchorClicked); } MsgCtxtView::~MsgCtxtView() { } const QString MsgCtxtView::BR = "
    "; void MsgCtxtView::cleanup() { m_unfinishedNotes.clear(); m_tempNotes.clear(); } void MsgCtxtView::gotoEntry(const DocPosition& pos, int selection) { m_entry = DocPos(pos); m_selection = selection; m_offset = pos.offset; QTimer::singleShot(0, this, &MsgCtxtView::process); QTimer::singleShot(0, this, &MsgCtxtView::pology); } void MsgCtxtView::process() { if (m_catalog->numberOfEntries() <= m_entry.entry) return;//because of Qt::QueuedConnection if (m_stackedLayout->currentIndex()) m_unfinishedNotes[m_prevEntry] = qMakePair(m_editor->note(), m_editor->noteIndex()); if (m_unfinishedNotes.contains(m_entry)) { addNoteUI(); m_editor->setNote(m_unfinishedNotes.value(m_entry).first, m_unfinishedNotes.value(m_entry).second); } else m_stackedLayout->setCurrentIndex(0); m_prevEntry = m_entry; m_browser->clear(); if (m_tempNotes.contains(m_entry.entry)) { QString html = i18nc("@info notes to translation unit which expire when the catalog is closed", "Temporary notes:"); html += MsgCtxtView::BR; foreach (const QString& note, m_tempNotes.values(m_entry.entry)) html += note.toHtmlEscaped() + MsgCtxtView::BR; html += MsgCtxtView::BR; m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR)); } QString phaseName = m_catalog->phase(m_entry.toDocPosition()); if (!phaseName.isEmpty()) { Phase phase = m_catalog->phase(phaseName); QString html = i18nc("@info translation unit metadata", "Phase:
    "); if (phase.date.isValid()) html += QString(QStringLiteral("%1: ")).arg(phase.date.toString(Qt::ISODate)); html += phase.process.toHtmlEscaped(); if (!phase.contact.isEmpty()) html += QString(QStringLiteral(" (%1)")).arg(phase.contact.toHtmlEscaped()); m_browser->insertHtml(html + MsgCtxtView::BR); } const QVector notes = m_catalog->notes(m_entry.toDocPosition()); m_hasErrorNotes = false; foreach (const Note& note, notes) m_hasErrorNotes = m_hasErrorNotes || note.content.contains(QLatin1String("[ERROR]")); int realOffset = displayNotes(m_browser, m_catalog->notes(m_entry.toDocPosition()), m_entry.form, m_catalog->capabilities()&MultipleNotes); QString html; foreach (const Note& note, m_catalog->developerNotes(m_entry.toDocPosition())) { html += MsgCtxtView::BR + escapeWithLinks(note.content); } QStringList sourceFiles = m_catalog->sourceFiles(m_entry.toDocPosition()); if (!sourceFiles.isEmpty()) { html += i18nc("@info PO comment parsing", "
    Files:
    "); foreach (const QString &sourceFile, sourceFiles) html += QString(QStringLiteral("%2
    ")).arg(sourceFile, sourceFile); html.chop(6); } QString msgctxt = m_catalog->context(m_entry.entry).first(); if (!msgctxt.isEmpty()) html += i18nc("@info PO comment parsing", "
    Context:
    ") + msgctxt.toHtmlEscaped(); QTextCursor t = m_browser->textCursor(); t.movePosition(QTextCursor::End); m_browser->setTextCursor(t); m_browser->insertHtml(html); t.movePosition(QTextCursor::Start); t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, realOffset + m_offset); t.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selection); m_browser->setTextCursor(t); } void MsgCtxtView::pology() { if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress == 0 && QFile::exists(m_catalog->url())) { QString command = Settings::self()->pologyCommandEntry(); command = command.replace(QStringLiteral("%u"), QString::number(m_entry.entry + 1)).replace(QStringLiteral("%f"), QStringLiteral("\"") + m_catalog->url() + QStringLiteral("\"")).replace(QStringLiteral("\n"), QStringLiteral(" ")); m_pologyProcess = new KProcess; m_pologyProcess->setShellCommand(command); m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); m_pologyStartedReceivingOutput = false; connect(m_pologyProcess, &KProcess::readyReadStandardOutput, this, &MsgCtxtView::pologyReceivedStandardOutput); connect(m_pologyProcess, &KProcess::readyReadStandardError, this, &MsgCtxtView::pologyReceivedStandardError); connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &MsgCtxtView::pologyHasFinished); m_pologyData = QStringLiteral("[pology] "); m_pologyProcessInProgress = m_entry.entry + 1; m_pologyProcess->start(); } else if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress > 0) { QTimer::singleShot(1000, this, &MsgCtxtView::pology); } } void MsgCtxtView::pologyReceivedStandardOutput() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; } const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput(); const QStringList pologyTmpLines = grossPologyOutput.split('\n', QString::SkipEmptyParts); foreach (const QString pologyTmp, pologyTmpLines) { if (pologyTmp.startsWith(QStringLiteral("[note]"))) m_pologyData += pologyTmp; } } } void MsgCtxtView::pologyReceivedStandardError() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; } m_pologyData += m_pologyProcess->readAllStandardError().replace('\n', MsgCtxtView::BR.toLatin1()); } } void MsgCtxtView::pologyHasFinished() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput(); const QStringList pologyTmpLines = grossPologyOutput.split('\n', QString::SkipEmptyParts); if (pologyTmpLines.count() == 0) { m_pologyData += i18nc("@info The pology command didn't return anything", "(empty)"); } else { foreach (const QString pologyTmp, pologyTmpLines) { if (pologyTmp.startsWith(QStringLiteral("[note]"))) m_pologyData += pologyTmp; } } } if (!m_tempNotes.value(m_entry.entry).startsWith(QStringLiteral("Failed rules:"))) { //This was not opened by pology //Delete the previous pology notes if (m_tempNotes.value(m_entry.entry).startsWith(QStringLiteral("[pology] "))) { m_tempNotes.remove(m_entry.entry); } addTemporaryEntryNote(m_entry.entry, m_pologyData); } } m_pologyProcess->deleteLater(); m_pologyProcessInProgress = 0; } void MsgCtxtView::addNoteUI() { anchorClicked(QUrl(QStringLiteral("note:/add"))); } void MsgCtxtView::anchorClicked(const QUrl& link) { QString path = link.path().mid(1); // minus '/' if (link.scheme() == QLatin1String("note")) { int capabilities = m_catalog->capabilities(); if (!m_editor) { m_editor = new NoteEditor(this); m_stackedLayout->addWidget(m_editor); connect(m_editor, &NoteEditor::accepted, this, &MsgCtxtView::noteEditAccepted); connect(m_editor, &NoteEditor::rejected, this, &MsgCtxtView::noteEditRejected); } m_editor->setNoteAuthors(m_catalog->noteAuthors()); QVector notes = m_catalog->notes(m_entry.toDocPosition()); int noteIndex = -1; //means add new note Note note; if (!path.endsWith(QLatin1String("add"))) { noteIndex = path.toInt(); note = notes.at(noteIndex); } else if (!(capabilities & MultipleNotes) && notes.size()) { noteIndex = 0; //so we don't overwrite the only possible note note = notes.first(); } m_editor->setNote(note, noteIndex); m_editor->setFromFieldVisible(capabilities & KeepsNoteAuthors); m_stackedLayout->setCurrentIndex(1); } else if (link.scheme() == QLatin1String("src")) { int pos = path.lastIndexOf(':'); emit srcFileOpenRequested(path.left(pos), path.midRef(pos + 1).toInt()); } else if (link.scheme().contains(QLatin1String("tp"))) QDesktopServices::openUrl(link); } void MsgCtxtView::noteEditAccepted() { DocPosition pos = m_entry.toDocPosition(); pos.form = m_editor->noteIndex(); m_catalog->push(new SetNoteCmd(m_catalog, pos, m_editor->note())); m_prevEntry.entry = -1; process(); //m_stackedLayout->setCurrentIndex(0); //m_unfinishedNotes.remove(m_entry); noteEditRejected(); } void MsgCtxtView::noteEditRejected() { m_stackedLayout->setCurrentIndex(0); m_unfinishedNotes.remove(m_entry); emit escaped(); } void MsgCtxtView::addNote(DocPosition p, const QString& text) { p.form = -1; m_catalog->push(new SetNoteCmd(m_catalog, p, Note(text))); if (m_entry.entry == p.entry) { m_prevEntry.entry = -1; process(); } } void MsgCtxtView::addTemporaryEntryNote(int entry, const QString& text) { m_tempNotes.insertMulti(entry, text); m_prevEntry.entry = -1; process(); } void MsgCtxtView::removeErrorNotes() { if (!m_hasErrorNotes) return; DocPosition p = m_entry.toDocPosition(); const QVector notes = m_catalog->notes(p); p.form = notes.size(); while (--(p.form) >= 0) { if (notes.at(p.form).content.contains(QLatin1String("[ERROR]"))) m_catalog->push(new SetNoteCmd(m_catalog, p, Note())); } m_prevEntry.entry = -1; process(); } diff --git a/src/msgctxtview.h b/src/msgctxtview.h index df69be2..6e71c4e 100644 --- a/src/msgctxtview.h +++ b/src/msgctxtview.h @@ -1,94 +1,95 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef MSGCTXTVIEW_H #define MSGCTXTVIEW_H #include "pos.h" #include "note.h" #include #include #include class Catalog; class NoteEditor; class QTextBrowser; class QStackedLayout; class MsgCtxtView: public QDockWidget { Q_OBJECT public: explicit MsgCtxtView(QWidget*, Catalog*); ~MsgCtxtView(); void gotoEntry(const DocPosition&, int selection = 0); void addNote(DocPosition, const QString& text); void addTemporaryEntryNote(int entry, const QString& text); public slots: void removeErrorNotes(); void cleanup(); void addNoteUI(); private slots: void anchorClicked(const QUrl& link); void noteEditAccepted(); void noteEditRejected(); void process(); void pology(); void pologyReceivedStandardOutput(); void pologyReceivedStandardError(); void pologyHasFinished(); signals: void srcFileOpenRequested(const QString& srcPath, int line); void escaped(); private: QTextBrowser* m_browser; NoteEditor* m_editor; QStackedLayout* m_stackedLayout; Catalog* m_catalog; QMap< DocPos, QPair > m_unfinishedNotes; //note and its index QMap< int, QString > m_tempNotes; int m_selection; int m_offset; bool m_hasInfo; bool m_hasErrorNotes; DocPos m_entry; DocPos m_prevEntry; KProcess* m_pologyProcess; int m_pologyProcessInProgress; bool m_pologyStartedReceivingOutput; QString m_pologyData; static const QString BR; }; #endif diff --git a/src/multieditoradaptor.h b/src/multieditoradaptor.h index 6f0c540..197af88 100644 --- a/src/multieditoradaptor.h +++ b/src/multieditoradaptor.h @@ -1,60 +1,61 @@ /* **************************************************************************** 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 MULTIEDITORADAPTOR_H #define MULTIEDITORADAPTOR_H #ifdef HAVE_CONFIG_H #include #endif #include "editoradaptor.h" /** * Hack over QDBusAbstractAdaptor to get kross active-editor-adaptor for free */ class MultiEditorAdaptor: public EditorAdaptor { Q_OBJECT public: explicit MultiEditorAdaptor(EditorTab *parent); ~MultiEditorAdaptor() { /*qCWarning(LOKALIZE_LOG)<<"bye bye cruel world";*/ } inline EditorTab* editorTab() const { return static_cast(QObject::parent()); } void setEditorTab(EditorTab* e); private slots: void handleParentDestroy(QObject* p); }; //methosa are defined in lokalizemainwindow.cpp #endif diff --git a/src/noteeditor.cpp b/src/noteeditor.cpp index 5837854..c2adb63 100644 --- a/src/noteeditor.cpp +++ b/src/noteeditor.cpp @@ -1,155 +1,156 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-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 "noteeditor.h" #include "lokalize_debug.h" #include "catalog.h" #include "cmd.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #include #include #include #include void TextEdit::keyPressEvent(QKeyEvent* keyEvent) { if (keyEvent->modifiers()& Qt::ControlModifier && keyEvent->key() == Qt::Key_Return) emit accepted(); else if (keyEvent->key() == Qt::Key_Escape) emit rejected(); else QPlainTextEdit::keyPressEvent(keyEvent); } NoteEditor::NoteEditor(QWidget* parent) : QWidget(parent) , m_from(new QComboBox(this)) , m_fromLabel(new QLabel(i18nc("@info:label", "From:"), this)) , m_authors(new QStringListModel(this)) , m_edit(new TextEdit(this)) , m_idx(-1) { setToolTip(i18nc("@info:tooltip", "Save empty note to remove it")); m_from->setToolTip(i18nc("@info:tooltip", "Author of this note")); m_from->setEditable(true); m_from->setModel(m_authors); m_from->completer()->setModel(m_authors); QVBoxLayout* main = new QVBoxLayout(this); QHBoxLayout* prop = new QHBoxLayout; main->addLayout(prop); prop->addWidget(m_fromLabel); prop->addWidget(m_from, 42); main->addWidget(m_edit); QDialogButtonBox* box = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Discard, this); box->button(QDialogButtonBox::Save)->setToolTip(i18n("Ctrl+Enter")); box->button(QDialogButtonBox::Discard)->setToolTip(i18n("Esc")); connect(m_edit, &TextEdit::accepted, this, &NoteEditor::accepted); connect(m_edit, &TextEdit::rejected, this, &NoteEditor::rejected); connect(box->button(QDialogButtonBox::Save), &QPushButton::clicked, this, &NoteEditor::accepted); connect(box->button(QDialogButtonBox::Discard), &QPushButton::clicked, this, &NoteEditor::rejected); main->addWidget(box); } void NoteEditor::setFromFieldVisible(bool v) { m_fromLabel->setVisible(v); m_from->setVisible(v); } Note NoteEditor::note() { m_note.content = m_edit->toPlainText(); m_note.from = m_from->currentText(); return m_note; } void NoteEditor::setNote(const Note& note, int idx) { m_note = note; m_edit->setPlainText(note.content); QString from = note.from; if (from.isEmpty()) from = Settings::authorName(); m_from->setCurrentText(from); QStringList l = m_authors->stringList(); if (!l.contains(from)) { l.append(from); m_authors->setStringList(l); } m_idx = idx; m_edit->setFocus(); } void NoteEditor::setNoteAuthors(const QStringList& authors) { m_authors->setStringList(authors); } int displayNotes(QTextBrowser* browser, const QVector< Note >& notes, int active, bool multiple) { QTextCursor t = browser->textCursor(); t.movePosition(QTextCursor::End); int realOffset = 0; static const QString BR = QStringLiteral("
    "); if (!notes.isEmpty()) { t.insertHtml(i18nc("@info XLIFF notes representation", "Notes:") + BR); int i = 0; foreach (const Note& note, notes) { if (!note.from.isEmpty()) t.insertHtml(QStringLiteral("") % note.from % QStringLiteral(": ")); if (i == active) realOffset = t.position(); QString content = escapeWithLinks(note.content); if (!multiple && content.contains('\n')) content += '\n'; content.replace('\n', BR); content += QString(QStringLiteral(" (")).arg(i) % i18nc("link to edit note", "edit...") % QStringLiteral(")
    "); t.insertHtml(content); i++; } if (multiple) t.insertHtml(QStringLiteral("") % i18nc("link to add a note", "Add...") % QStringLiteral(" ")); } else browser->insertHtml(QStringLiteral("") % i18nc("link to add a note", "Add a note...") % QStringLiteral(" ")); return realOffset; } diff --git a/src/noteeditor.h b/src/noteeditor.h index 5b6eec0..2e78f3e 100644 --- a/src/noteeditor.h +++ b/src/noteeditor.h @@ -1,81 +1,82 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 NOTEEDITOR_H #define NOTEEDITOR_H #include "note.h" #include class QStringListModel; class QLabel; class QTextBrowser; class QComboBox; class TextEdit; int displayNotes(QTextBrowser* browser, const QVector< Note >& notes, int active = 0, bool multiple = true); QString escapeWithLinks(const QString& text);//defined in htmlhelpers.cpp class NoteEditor: public QWidget { Q_OBJECT public: explicit NoteEditor(QWidget* parent); ~NoteEditor() {} Note note(); void setNote(const Note&, int idx); int noteIndex() { return m_idx; } void setNoteAuthors(const QStringList&); void setFromFieldVisible(bool); signals: void accepted(); void rejected(); private: QComboBox* m_from; QLabel* m_fromLabel; QStringListModel* m_authors; TextEdit* m_edit; int m_idx; Note m_note; }; class TextEdit: public QPlainTextEdit { Q_OBJECT public: explicit TextEdit(QWidget* parent): QPlainTextEdit(parent) {} void keyPressEvent(QKeyEvent* e) override; signals: void accepted(); void rejected(); }; #endif diff --git a/src/phaseswindow.cpp b/src/phaseswindow.cpp index 58f91f6..52ab4d7 100644 --- a/src/phaseswindow.cpp +++ b/src/phaseswindow.cpp @@ -1,360 +1,361 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2009-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 "phaseswindow.h" #include "catalog.h" #include "cmd.h" #include "noteeditor.h" #include "project.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //BEGIN PhasesModel class PhasesModel: public QAbstractListModel { public: enum PhasesModelColumns { Date = 0, Process, Company, Contact, ToolName, ColumnCount }; PhasesModel(Catalog* catalog, QObject* parent); ~PhasesModel() {} QModelIndex addPhase(const Phase& phase); QModelIndex activePhaseIndex()const { return index(m_activePhase); } QList addedPhases()const; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent); return ColumnCount; } QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; private: Catalog* m_catalog; QList m_phases; QMap m_tools; int m_activePhase; }; PhasesModel::PhasesModel(Catalog* catalog, QObject* parent) : QAbstractListModel(parent) , m_catalog(catalog) , m_phases(catalog->allPhases()) , m_tools(catalog->allTools()) { m_activePhase = m_phases.size(); while (--m_activePhase >= 0 && m_phases.at(m_activePhase).name != catalog->activePhase()) ; } QModelIndex PhasesModel::addPhase(const Phase& phase) { m_activePhase = m_phases.size(); beginInsertRows(QModelIndex(), m_activePhase, m_activePhase); m_phases.append(phase); endInsertRows(); return index(m_activePhase); } QList PhasesModel::addedPhases()const { QList result; for (int i = m_catalog->allPhases().size(); i < m_phases.size(); ++i) result.append(m_phases.at(i)); return result; } int PhasesModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_phases.size(); } QVariant PhasesModel::data(const QModelIndex& index, int role) const { if (role == Qt::FontRole && index.row() == m_activePhase) { QFont font = QApplication::font(); font.setBold(true); return font; } if (role == Qt::UserRole) return m_phases.at(index.row()).name; if (role != Qt::DisplayRole) return QVariant(); const Phase& phase = m_phases.at(index.row()); switch (index.column()) { case Date: return phase.date.toString(); case Process: return phase.process; case Company: return phase.company; case Contact: return QString(phase.contact % (phase.email.isEmpty() ? QString() : QStringLiteral(" <%1> ").arg(phase.email)) % (phase.phone.isEmpty() ? QString() : QStringLiteral(", %1").arg(phase.phone))); case ToolName: return m_tools.value(phase.tool).name; } return QVariant(); } QVariant PhasesModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case Date: return i18nc("@title:column", "Date"); case Process: return i18nc("@title:column", "Process"); case Company: return i18nc("@title:column", "Company"); case Contact: return i18nc("@title:column", "Person"); case ToolName: return i18nc("@title:column", "Tool"); } return QVariant(); } //END PhasesModel //BEGIN PhaseEditDialog class PhaseEditDialog: public QDialog { public: PhaseEditDialog(QWidget *parent); ~PhaseEditDialog() {} Phase phase()const; ProjectLocal::PersonRole role()const; private: KComboBox* m_process; }; PhaseEditDialog::PhaseEditDialog(QWidget *parent) : QDialog(parent) , m_process(new KComboBox(this)) { QStringList processes; processes << i18n("Translation") << i18n("Review") << i18n("Approval"); m_process->setModel(new QStringListModel(processes, this)); QFormLayout* l = new QFormLayout(this); l->addRow(i18nc("noun", "Process (this will also change your role):"), m_process); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(buttonBox, &QDialogButtonBox::accepted, this, &PhaseEditDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &PhaseEditDialog::reject); l->addRow(buttonBox); } Phase PhaseEditDialog::phase() const { Phase phase; phase.process = processes()[m_process->currentIndex()]; return phase; } ProjectLocal::PersonRole PhaseEditDialog::role() const { return (ProjectLocal::PersonRole)m_process->currentIndex(); } PhasesWindow::PhasesWindow(Catalog* catalog, QWidget *parent) : QDialog(parent) , m_catalog(catalog) , m_model(new PhasesModel(catalog, this)) , m_view(new MyTreeView(this)) , m_browser(new QTextBrowser(this)) , m_editor(0) { connect(this, &PhasesWindow::accepted, this, &PhasesWindow::handleResult); //setAttribute(Qt::WA_DeleteOnClose, true); QVBoxLayout* l = new QVBoxLayout(this); QHBoxLayout* btns = new QHBoxLayout; l->addLayout(btns); QPushButton* add = new QPushButton(this); KGuiItem::assign(add, KStandardGuiItem::add()); connect(add, &QPushButton::clicked, this, &PhasesWindow::addPhase); btns->addWidget(add); btns->addStretch(5); QSplitter* splitter = new QSplitter(this); l->addWidget(splitter); m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &PhasesWindow::accept); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &PhasesWindow::reject); l->addWidget(m_buttonBox); m_view->setRootIsDecorated(false); m_view->setModel(m_model); splitter->addWidget(m_view); int column = m_model->columnCount(); while (--column >= 0) m_view->resizeColumnToContents(column); if (m_model->rowCount()) m_view->setCurrentIndex(m_model->activePhaseIndex()); connect(m_view, &MyTreeView::currentIndexChanged, this, &PhasesWindow::displayPhaseNotes); m_noteView = new QWidget(this); m_noteView->hide(); splitter->addWidget(m_noteView); m_stackedLayout = new QStackedLayout(m_noteView); m_stackedLayout->addWidget(m_browser); m_browser->viewport()->setBackgroundRole(QPalette::Background); m_browser->setOpenLinks(false); connect(m_browser, &QTextBrowser::anchorClicked, this, &PhasesWindow::anchorClicked); splitter->setStretchFactor(0, 15); splitter->setStretchFactor(1, 5); resize(QSize(700, 400)); } void PhasesWindow::handleResult() { m_catalog->beginMacro(i18nc("@item Undo action item", "Edit phases")); Phase last; foreach (const Phase& phase, m_model->addedPhases()) static_cast(m_catalog)->push(new UpdatePhaseCmd(m_catalog, last = phase)); Project::instance()->local()->setRole(roleForProcess(last.process)); m_catalog->setActivePhase(last.name, roleForProcess(last.process)); QMapIterator > i(m_phaseNotes); while (i.hasNext()) { i.next(); m_catalog->setPhaseNotes(i.key(), i.value()); } m_catalog->endMacro(); } void PhasesWindow::addPhase() { PhaseEditDialog d(this); if (!d.exec()) return; Phase phase = d.phase(); initPhaseForCatalog(m_catalog, phase, ForceAdd); m_view->setCurrentIndex(m_model->addPhase(phase)); m_phaseNotes.insert(phase.name, QVector()); m_buttonBox->button(QDialogButtonBox::Ok)->setFocus(); } static QString phaseNameFromView(QTreeView* view) { return view->currentIndex().data(Qt::UserRole).toString(); } void PhasesWindow::anchorClicked(QUrl link) { QString path = link.path().mid(1); // minus '/' if (link.scheme() == QLatin1String("note")) { if (!m_editor) { m_editor = new NoteEditor(this); m_stackedLayout->addWidget(m_editor); connect(m_editor, &NoteEditor::accepted, this, &PhasesWindow::noteEditAccepted); connect(m_editor, &NoteEditor::rejected, this, &PhasesWindow::noteEditRejected); } m_editor->setNoteAuthors(m_catalog->noteAuthors()); if (path.endsWith(QLatin1String("add"))) m_editor->setNote(Note(), -1); else { int pos = path.toInt(); QString phaseName = phaseNameFromView(m_view); QVector notes = m_phaseNotes.contains(phaseName) ? m_phaseNotes.value(phaseName) : m_catalog->phaseNotes(phaseName); m_editor->setNote(notes.at(pos), pos); } m_stackedLayout->setCurrentIndex(1); } } void PhasesWindow::noteEditAccepted() { QString phaseName = phaseNameFromView(m_view); if (!m_phaseNotes.contains(phaseName)) m_phaseNotes.insert(phaseName, m_catalog->phaseNotes(phaseName)); //QVector notes=m_phaseNotes.value(phaseName); if (m_editor->noteIndex() == -1) m_phaseNotes[phaseName].append(m_editor->note()); else m_phaseNotes[phaseName][m_editor->noteIndex()] = m_editor->note(); m_stackedLayout->setCurrentIndex(0); displayPhaseNotes(m_view->currentIndex()); } void PhasesWindow::noteEditRejected() { m_stackedLayout->setCurrentIndex(0); } void PhasesWindow::displayPhaseNotes(const QModelIndex& current) { m_browser->clear(); QString phaseName = current.data(Qt::UserRole).toString(); QVector notes = m_phaseNotes.contains(phaseName) ? m_phaseNotes.value(phaseName) : m_catalog->phaseNotes(phaseName); displayNotes(m_browser, notes); m_noteView->show(); m_stackedLayout->setCurrentIndex(0); } diff --git a/src/phaseswindow.h b/src/phaseswindow.h index 74b2ae5..c9162f4 100644 --- a/src/phaseswindow.h +++ b/src/phaseswindow.h @@ -1,94 +1,95 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 PHASESWINDOW_H #define PHASESWINDOW_H #include "phase.h" #include "note.h" #include #include #include #include #include class QDialogButtonBox; class QStackedLayout; class QTextBrowser; class NoteEditor; class PhasesModel; class MyTreeView; class PhasesWindow: public QDialog { Q_OBJECT public: explicit PhasesWindow(Catalog* catalog, QWidget *parent); ~PhasesWindow() {} private slots: void displayPhaseNotes(const QModelIndex& current); void addPhase(); void handleResult(); void anchorClicked(QUrl); void noteEditAccepted(); void noteEditRejected(); private: Catalog* m_catalog; PhasesModel* m_model; MyTreeView* m_view; QTextBrowser* m_browser; NoteEditor* m_editor; QWidget* m_noteView; QStackedLayout* m_stackedLayout; QDialogButtonBox* m_buttonBox; QMap > m_phaseNotes; }; #include class MyTreeView: public QTreeView { Q_OBJECT public: explicit MyTreeView(QWidget* parent): QTreeView(parent) {} ~MyTreeView() override {} signals: void currentIndexChanged(const QModelIndex& current); private: void currentChanged(const QModelIndex& current, const QModelIndex&) override { emit currentIndexChanged(current); } }; #endif diff --git a/src/prefs/prefs.cpp b/src/prefs/prefs.cpp index 66a5fc2..00d0c3e 100644 --- a/src/prefs/prefs.cpp +++ b/src/prefs/prefs.cpp @@ -1,397 +1,398 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "prefs.h" #include "lokalize_debug.h" #include "prefs_lokalize.h" #include "project.h" #include "projectlocal.h" #include "projectmodel.h" #include "languagelistmodel.h" #include "dbfilesmodel.h" #include "ui_prefs_identity.h" #include "ui_prefs_editor.h" #include "ui_prefs_general.h" #include "ui_prefs_appearance.h" #include "ui_prefs_pology.h" #include "ui_prefs_tm.h" #include "ui_prefs_projectmain.h" #include "ui_prefs_project_advanced.h" #include "ui_prefs_project_local.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include SettingsController* SettingsController::_instance = 0; void SettingsController::cleanupSettingsController() { delete SettingsController::_instance; SettingsController::_instance = 0; } SettingsController* SettingsController::instance() { if (_instance == 0) { _instance = new SettingsController; qAddPostRoutine(SettingsController::cleanupSettingsController); } return _instance; } SettingsController::SettingsController() : QObject(Project::instance()) , dirty(false) , m_projectActionsView(0) , m_mainWindowPtr(0) {} SettingsController::~SettingsController() {} void SettingsController::showSettingsDialog() { if (KConfigDialog::showDialog("lokalize_settings")) return; KConfigDialog *dialog = new KConfigDialog(m_mainWindowPtr, "lokalize_settings", Settings::self()); dialog->setFaceType(KPageDialog::List); // Identity QWidget *w = new QWidget(dialog); Ui_prefs_identity ui_prefs_identity; ui_prefs_identity.setupUi(w); KConfigGroup grp = Settings::self()->config()->group("Identity"); ui_prefs_identity.DefaultLangCode->setModel(LanguageListModel::instance()->sortModel()); ui_prefs_identity.DefaultLangCode->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(grp.readEntry("DefaultLangCode", QLocale::system().name()))); connect(ui_prefs_identity.DefaultLangCode, QOverload::of(&KComboBox::activated), ui_prefs_identity.kcfg_DefaultLangCode, &LangCodeSaver::setLangCode); ui_prefs_identity.kcfg_DefaultLangCode->hide(); dialog->addPage(w, i18nc("@title:tab", "Identity"), "preferences-desktop-user"); //General w = new QWidget(dialog); Ui_prefs_general ui_prefs_general; ui_prefs_general.setupUi(w); connect(ui_prefs_general.kcfg_CustomEditorEnabled, &QCheckBox::toggled, ui_prefs_general.kcfg_CustomEditorCommand, &QLineEdit::setEnabled); ui_prefs_general.kcfg_CustomEditorCommand->setEnabled(Settings::self()->customEditorEnabled()); dialog->addPage(w, i18nc("@title:tab", "General"), "preferences-system-windows"); //Editor w = new QWidget(dialog); Ui_prefs_editor ui_prefs_editor; ui_prefs_editor.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Editing"), "accessories-text-editor"); //Font w = new QWidget(dialog); Ui_prefs_appearance ui_prefs_appearance; ui_prefs_appearance.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Appearance"), "preferences-desktop-font"); //TM w = new QWidget(dialog); Ui_prefs_tm ui_prefs_tm; ui_prefs_tm.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Translation Memory"), "configure"); //Pology w = new QWidget(dialog); Ui_prefs_pology ui_prefs_pology; ui_prefs_pology.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Pology"), "preferences-desktop-filetype-association"); connect(dialog, &KConfigDialog::settingsChanged, this, &SettingsController::generalSettingsChanged); //Spellcheck #if 0 w = new Sonnet::ConfigWidget(Settings::self()->config(), dialog); w->setParent(this); dialog->addPage(w, i18nc("@title:tab", "Spellcheck"), "spellcheck_setting"); connect(dialog, SIGNAL(okClicked()), w, SLOT(save())); connect(dialog, SIGNAL(applyClicked()), w, SLOT(save())); connect(dialog, SIGNAL(defaultClicked()), w, SLOT(slotDefault())); #endif //connect(dialog,SIGNAL(settingsChanged(const QString&)),m_view, SLOT(settingsChanged())); dialog->show(); // dialog->addPage(new General(0, "General"), i18n("General") ); // dialog->addPage(new Appearance(0, "Style"), i18n("Appearance") ); // connect(dialog, SIGNAL(settingsChanged(const QString&)), mainWidget, SLOT(loadSettings())); // connect(dialog, SIGNAL(settingsChanged(const QString&)), this, SLOT(loadSettings())); } ScriptsView::ScriptsView(QWidget* parent): Kross::ActionCollectionView(parent) { setAcceptDrops(true); } void ScriptsView::dragEnterEvent(QDragEnterEvent* event) { if (!event->mimeData()->urls().isEmpty() && event->mimeData()->urls().first().path().endsWith(QLatin1String(".rc"))) event->accept(); } void ScriptsView::dropEvent(QDropEvent* event) { Kross::ActionCollectionModel* scriptsModel = static_cast(model()); foreach (const QUrl& url, event->mimeData()->urls()) if (url.path().endsWith(QLatin1String(".rc"))) scriptsModel->rootCollection()->readXmlFile(url.path()); } bool SettingsController::ensureProjectIsLoaded() { if (Project::instance()->isLoaded()) return true; int answer = KMessageBox::questionYesNoCancel(m_mainWindowPtr, i18n("You have accessed a feature that requires a project to be loaded. Do you want to create a new project or open an existing project?"), QString(), KGuiItem(i18nc("@action", "New"), QIcon::fromTheme("document-new")), KGuiItem(i18nc("@action", "Open"), QIcon::fromTheme("project-open")) ); if (answer == KMessageBox::Yes) return projectCreate(); if (answer == KMessageBox::No) return !projectOpen().isEmpty(); return false; } QString SettingsController::projectOpen(QString path, bool doOpen) { if (path.isEmpty()) { //Project::instance()->model()->weaver()->suspend(); //KDE5PORT mutex if needed path = QFileDialog::getOpenFileName(m_mainWindowPtr, QString(), QDir::homePath()/*_catalog->url().directory()*/, i18n("Lokalize translation project (*.lokalize)")/*"text/x-lokalize-project"*/); //Project::instance()->model()->weaver()->resume(); } if (!path.isEmpty() && doOpen) Project::instance()->load(path); return path; } bool SettingsController::projectCreate() { //Project::instance()->model()->weaver()->suspend(); //KDE5PORT mutex if needed QString desirablePath = Project::instance()->desirablePath(); if (desirablePath.isEmpty()) desirablePath = QDir::homePath() + "/index.lokalize"; QString path = QFileDialog::getSaveFileName(m_mainWindowPtr, i18nc("@window:title", "Select folder with Gettext .po files to translate"), desirablePath, i18n("Lokalize translation project (*.lokalize)") /*"text/x-lokalize-project"*/); //Project::instance()->model()->weaver()->resume(); if (path.isEmpty()) return false; if (m_projectActionsView && m_projectActionsView->model()) { //ActionCollectionModel is known to be have bad for the usecase of reinitializing krossplugin m_projectActionsView->model()->deleteLater(); m_projectActionsView->setModel(nullptr); } //TODO ask-n-save QDir projectFolder = QFileInfo(path).absoluteDir(); QString projectId = projectFolder.dirName(); if (projectFolder.cdUp()) projectId = projectFolder.dirName() % '-' % projectId;; Project::instance()->load(path, QString(), projectId); //Project::instance()->setDefaults(); //NOTE will this be an obstacle? //Project::instance()->setProjectID(); QTimer::singleShot(500, this, &SettingsController::projectConfigure); return true; } void SettingsController::projectConfigure() { if (Project::instance()->path().isEmpty()) { KMessageBox::error(mainWindowPtr(), i18n("Create software or OpenDocument translation project first.")); return; } if (KConfigDialog::showDialog("project_settings")) { if (!m_projectActionsView->model()) m_projectActionsView->setModel(new Kross::ActionCollectionModel(m_projectActionsView, Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()))); return; } KConfigDialog *dialog = new KConfigDialog(m_mainWindowPtr, "project_settings", Project::instance()); dialog->setFaceType(KPageDialog::List); // Main QWidget *w = new QWidget(dialog); Ui_prefs_projectmain ui_prefs_projectmain; ui_prefs_projectmain.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "General"), "preferences-desktop-locale"); ui_prefs_projectmain.kcfg_LangCode->hide(); ui_prefs_projectmain.kcfg_PoBaseDir->hide(); ui_prefs_projectmain.kcfg_GlossaryTbx->hide(); Project& p = *(Project::instance()); ui_prefs_projectmain.LangCode->setModel(LanguageListModel::instance()->sortModel()); ui_prefs_projectmain.LangCode->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(p.langCode())); connect(ui_prefs_projectmain.LangCode, QOverload::of(&KComboBox::activated), ui_prefs_projectmain.kcfg_LangCode, &LangCodeSaver::setLangCode); ui_prefs_projectmain.poBaseDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_prefs_projectmain.glossaryTbx->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); ui_prefs_projectmain.glossaryTbx->setFilter("*.tbx\n*.xml"); connect(ui_prefs_projectmain.poBaseDir, &KUrlRequester::textChanged, ui_prefs_projectmain.kcfg_PoBaseDir, &RelPathSaver::setText); connect(ui_prefs_projectmain.glossaryTbx, &KUrlRequester::textChanged, ui_prefs_projectmain.kcfg_GlossaryTbx, &RelPathSaver::setText); ui_prefs_projectmain.poBaseDir->setUrl(QUrl::fromLocalFile(p.poDir())); ui_prefs_projectmain.glossaryTbx->setUrl(QUrl::fromLocalFile(p.glossaryPath())); // RegExps w = new QWidget(dialog); Ui_project_advanced ui_project_advanced; ui_project_advanced.setupUi(w); ui_project_advanced.kcfg_PotBaseDir->hide(); ui_project_advanced.kcfg_BranchDir->hide(); ui_project_advanced.kcfg_AltDir->hide(); ui_project_advanced.potBaseDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_project_advanced.branchDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_project_advanced.altDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); connect(ui_project_advanced.potBaseDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_PotBaseDir, &RelPathSaver::setText); connect(ui_project_advanced.branchDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_BranchDir, &RelPathSaver::setText); connect(ui_project_advanced.altDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_AltDir, &RelPathSaver::setText); ui_project_advanced.potBaseDir->setUrl(QUrl::fromLocalFile(p.potDir())); ui_project_advanced.branchDir->setUrl(QUrl::fromLocalFile(p.branchDir())); ui_project_advanced.altDir->setUrl(QUrl::fromLocalFile(p.altTransDir())); dialog->addPage(w, i18nc("@title:tab", "Advanced"), "applications-development-translation"); //Scripts w = new QWidget(dialog); QVBoxLayout* layout = new QVBoxLayout(w); layout->setSpacing(6); layout->setMargin(11); //m_projectActionsEditor=new Kross::ActionCollectionEditor(Kross::Manager::self().actionCollection()->collection(Project::instance()->projectID()),w); m_projectActionsView = new ScriptsView(w); layout->addWidget(m_projectActionsView); m_projectActionsView->setModel(new Kross::ActionCollectionModel(w, Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()))); QHBoxLayout* btns = new QHBoxLayout(); layout->addLayout(btns); btns->addWidget(m_projectActionsView->createButton(w, "edit")); dialog->addPage(w, i18nc("@title:tab", "Scripts"), "preferences-system-windows-actions"); w = new QWidget(dialog); Ui_prefs_project_local ui_prefs_project_local; ui_prefs_project_local.setupUi(w); dialog->addPage(w, Project::local(), i18nc("@title:tab", "Personal"), "preferences-desktop-user"); connect(dialog, &KConfigDialog::settingsChanged, Project::instance(), &Project::reinit); connect(dialog, &KConfigDialog::settingsChanged, Project::instance(), &Project::save, Qt::QueuedConnection); connect(dialog, &KConfigDialog::settingsChanged, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex); connect(dialog, &KConfigDialog::settingsChanged, this, &SettingsController::reflectProjectConfigChange); dialog->show(); } void SettingsController::reflectProjectConfigChange() { //TODO check target language change: reflect changes in TM and glossary TM::DBFilesModel::instance()->openDB(Project::instance()->projectID()); } void SettingsController::reflectRelativePathsHack() { //m_scriptsRelPrefWidget->clear(); QStringList actionz(m_scriptsPrefWidget->items()); QString projectDir(Project::instance()->projectDir()); int i = actionz.size(); while (--i >= 0) actionz[i] = QDir(projectDir).relativeFilePath(actionz.at(i)); m_scriptsRelPrefWidget->setItems(actionz); } void LangCodeSaver::setLangCode(int index) { setText(LanguageListModel::instance()->langCodeForSortModelRow(index)); } void RelPathSaver::setText(const QString& txt) { QLineEdit::setText(QDir(Project::instance()->projectDir()).relativeFilePath(txt)); } void writeUiState(const char* elementName, const QByteArray& state) { KConfig config; KConfigGroup cg(&config, "MainWindow"); cg.writeEntry(elementName, state.toBase64()); } QByteArray readUiState(const char* elementName) { KConfig config; KConfigGroup cg(&config, "MainWindow"); return QByteArray::fromBase64(cg.readEntry(elementName, QByteArray())); } diff --git a/src/prefs/prefs.h b/src/prefs/prefs.h index 2380701..30a3b87 100644 --- a/src/prefs/prefs.h +++ b/src/prefs/prefs.h @@ -1,130 +1,131 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef PREFS_H #define PREFS_H #include class KEditListWidget; namespace Kross { class ActionCollectionView; } /** * Singleton that manages cfgs for Lokalize and projects */ class SettingsController: public QObject { Q_OBJECT public: SettingsController(); ~SettingsController(); bool dirty; void setMainWindowPtr(QWidget* w) { m_mainWindowPtr = w; } QWidget* mainWindowPtr() { return m_mainWindowPtr; } public slots: void showSettingsDialog(); bool ensureProjectIsLoaded(); QString projectOpen(QString path = QString(), bool doOpen = true); bool projectCreate(); void projectConfigure(); void reflectProjectConfigChange(); void reflectRelativePathsHack(); signals: void generalSettingsChanged(); private: KEditListWidget* m_scriptsRelPrefWidget; //HACK to get relative filenames in the project file KEditListWidget* m_scriptsPrefWidget; Kross::ActionCollectionView* m_projectActionsView; QWidget* m_mainWindowPtr; private: static SettingsController* _instance; static void cleanupSettingsController(); public: static SettingsController* instance(); }; /** * helper widget to save relative paths in project file, * thus allowing its publishing in e.g. svn */ class RelPathSaver: public QLineEdit { Q_OBJECT public: explicit RelPathSaver(QWidget* p): QLineEdit(p) {} public slots: void setText(const QString&); }; /** * helper widget to save lang code text values * identified by LanguageListModel string index internally */ class LangCodeSaver: public QLineEdit { Q_OBJECT public: explicit LangCodeSaver(QWidget* p): QLineEdit(p) {} public slots: void setLangCode(int); }; #include class ScriptsView: public Kross::ActionCollectionView { Q_OBJECT public: explicit ScriptsView(QWidget* parent); // public slots: // void addScsetText(const QString&); void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; }; void writeUiState(const char* elementName, const QByteArray&); QByteArray readUiState(const char* elementName); #endif diff --git a/src/project/kde-i18n-lists.h b/src/project/kde-i18n-lists.h index 52cf884..c7f545b 100644 --- a/src/project/kde-i18n-lists.h +++ b/src/project/kde-i18n-lists.h @@ -1,31 +1,32 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2008 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 LISTS_H #define LISTS_H #include QString getMailingList(); //implementation is in project.cpp #endif diff --git a/src/project/poextractor.cpp b/src/project/poextractor.cpp index 112ae4f..6d14680 100644 --- a/src/project/poextractor.cpp +++ b/src/project/poextractor.cpp @@ -1,189 +1,190 @@ /* Gettext translation file analyzer Copyright (C) 2007 Montel Laurent Copyright (C) 2009 Jos van den Oever Copyright (C) 2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "poextractor.h" #include #include POExtractor::POExtractor() : state(WHITESPACE) , messages(0) , untranslated(0) , fuzzy(0) , isFuzzy(false) , isTranslated(false) { } void POExtractor::endMessage() { messages++; if (isTranslated) fuzzy += isFuzzy; untranslated += (!isTranslated); isFuzzy = false; isTranslated = false; state = WHITESPACE; } void POExtractor::handleComment(const char* data, uint32_t length) { state = COMMENT; if (length >= 8 && strncmp(data, "#, fuzzy", 8) == 0) { // could be better isFuzzy = true; } } void POExtractor::handleLine(const char* data, uint32_t length) { if (state == ERROR) return; if (state == WHITESPACE) { if (length == 0) return; if (data[0] != '#') { state = COMMENT; //this allows PO files w/o comments } else { handleComment(data, length); return; } } if (state == COMMENT) { if (length == 0) { state = WHITESPACE; } else if (data[0] == '#') { handleComment(data, length); } else if (length > 7 && strncmp("msgctxt", data, 7) == 0) { state = MSGCTXT; } else if (length > 7 && strncmp("msgid \"", data, 7) == 0) { state = MSGID; } else { state = ERROR; } return; } else if (length > 1 && data[0] == '"' && data[length - 1] == '"' && (state == MSGCTXT || state == MSGID || state == MSGSTR || state == MSGID_PLURAL)) { // continued text field isTranslated = state == MSGSTR && length > 2; } else if (state == MSGCTXT && length > 7 && strncmp("msgid \"", data, 7) == 0) { state = MSGID; } else if (state == MSGID && length > 14 && strncmp("msgid_plural \"", data, 14) == 0) { state = MSGID_PLURAL; } else if ((state == MSGID || state == MSGID_PLURAL || state == MSGSTR) && length > 8 && strncmp("msgstr", data, 6) == 0) { state = MSGSTR; isTranslated = strncmp(data + length - 3, " \"\"", 3) != 0; } else if (state == MSGSTR) { if (length == 0) { endMessage(); } else if (data[0] == '#' || data[0] == 'm') { //allow PO without empty line between entries endMessage(); state = COMMENT; handleLine(data, length); } else { state = ERROR; } } else { state = ERROR; } #if 0 if (messages > 1 || state != MSGSTR) return; // handle special values in the first messsage // assumption is that value takes up only one line if (strncmp("\"POT-Creation-Date: ", data, 20) == 0) { result->add(Property::TranslationTemplateDate, QByteArray(data + 20, length - 21)); } else if (strncmp("\"PO-Revision-Date: ", data, 19) == 0) { result->add(Property::TranslationLastUpDate, QByteArray(data + 19, length - 20)); } else if (strncmp("\"Last-Translator: ", data, 18) == 0) { result->add(Property::TranslationLastAuthor, QByteArray(data + 18, length - 19)); } #endif } void POExtractor::extract(const QString& filePath, FileMetaData& m) { std::ifstream fstream(QFile::encodeName(filePath)); if (!fstream.is_open()) { return; } state = WHITESPACE; messages = 0; untranslated = 0; fuzzy = 0; isFuzzy = false; isTranslated = false; std::string line; int lines = 0; while (std::getline(fstream, line)) { //TODO add a parsed text of translation units //QByteArray arr = QByteArray::fromRawData(line.c_str(), line.size()); //result->append(QString::fromUtf8(arr)); handleLine(line.c_str(), line.size()); lines++; if (messages <= 1 && state == MSGSTR) { // handle special values in the first messsage // assumption is that value takes up only one line if (strncmp("\"POT-Creation-Date: ", line.c_str(), 20) == 0) { m.sourceDate = QByteArray(line.c_str() + 20, line.size() - 21 - 2); } else if (strncmp("\"PO-Revision-Date: ", line.c_str(), 19) == 0) { m.translationDate = QByteArray(line.c_str() + 19, line.size() - 20 - 2); } else if (strncmp("\"Last-Translator: ", line.c_str(), 18) == 0) { m.lastTranslator = QString::fromUtf8(QByteArray::fromRawData(line.c_str() + 18, line.size() - 19 - 2)); } fuzzy = 0; } } handleLine("", 0); //for files with non-empty last line messages--;//cause header does not count /* result->add(Property::TranslationUnitsTotal, messages); result->add(Property::TranslationUnitsWithTranslation, messages-untranslated); result->add(Property::TranslationUnitsWithDraftTranslation, fuzzy); result->add(Property::LineCount, lines); */ //TODO WordCount m.fuzzy = fuzzy; m.translated = messages - untranslated - fuzzy; m.untranslated = untranslated; m.filePath = filePath; //File is invalid if (messages < 0 || fuzzy < 0 || untranslated < 0) { m.invalid_file = true; m.translated = 0; m.untranslated = 0; m.fuzzy = 0; } //TODO m.translated_approver = m.translated_reviewer = m.translated; m.fuzzy_approver = m.fuzzy_reviewer = m.fuzzy; } diff --git a/src/project/poextractor.h b/src/project/poextractor.h index 61090fb..bd62dd9 100644 --- a/src/project/poextractor.h +++ b/src/project/poextractor.h @@ -1,53 +1,54 @@ /* Gettext translation file analyzer Copyright (C) 2007 Montel Laurent Copyright (C) 2009 Jos van den Oever Copyright (C) 2014 Nick Shaforostoff + 2018-2019 by Simon Depiets This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef POEXTRACTOR_H #define POEXTRACTOR_H #include "projectmodel.h" class POExtractor { public: POExtractor(); void extract(const QString& filePath, FileMetaData& data); private: void endMessage(); void handleComment(const char* data, uint32_t length); void handleLine(const char* data, uint32_t length); enum PoState {COMMENT, MSGCTXT, MSGID, MSGID_PLURAL, MSGSTR, MSGSTR_PLURAL, WHITESPACE, ERROR }; PoState state; int messages; int untranslated; int fuzzy; bool isFuzzy, isTranslated; }; #endif // PLAINTEXTEXTRACTOR_H diff --git a/src/project/project.cpp b/src/project/project.cpp index 7da0d21..495144e 100644 --- a/src/project/project.cpp +++ b/src/project/project.cpp @@ -1,498 +1,499 @@ /* **************************************************************************** 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 "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) { QTime a; a.start(); TM::threadPool()->clear(); qCDebug(LOKALIZE_LOG) << "loading" << newProjectPath << "finishing tm jobs..."; if (!m_path.isEmpty()) { TM::CloseDBJob* closeDBJob = new TM::CloseDBJob(projectID()); closeDBJob->setAutoDelete(true); TM::threadPool()->start(closeDBJob, CLOSEDB); } TM::threadPool()->waitForDone(500);//more safety setSharedConfig(KSharedConfig::openConfig(newProjectPath, KConfig::NoGlobals)); if (!QFileInfo::exists(newProjectPath)) Project::instance()->setDefaults(); ProjectBase::load(); m_path = newProjectPath; m_desirablePath.clear(); //cache: m_projectDir = QFileInfo(m_path).absolutePath(); m_localConfig->setSharedConfig(KSharedConfig::openConfig(projectID() + QStringLiteral(".local"), KConfig::NoGlobals, QStandardPaths::DataLocation)); m_localConfig->load(); if (forcedTargetLangCode.length()) setLangCode(forcedTargetLangCode); else if (langCode().isEmpty()) setLangCode(QLocale::system().name()); if (forcedProjectId.length()) setProjectID(forcedProjectId); //KConfig config; //delete m_localConfig; m_localConfig=new KConfigGroup(&config,"Project-"+path()); populateDirModel(); //put 'em into thread? //QTimer::singleShot(0,this,SLOT(populateGlossary())); populateGlossary();//we cant postpone it because project load can be called from define new term function m_sourceFilePaths.clear(); if (newProjectPath.isEmpty()) return; if (!isTmSupported()) qCWarning(LOKALIZE_LOG) << "no sqlite module available"; //NOTE do we need to explicitly call it when project id changes? TM::DBFilesModel::instance()->openDB(projectID(), TM::Undefined, true); if (QaModel::isInstantiated()) { QaModel::instance()->saveRules(); QaModel::instance()->loadRules(qaPath()); } //qCDebug(LOKALIZE_LOG)<<"until emitting signal"<setAutoDelete(true); TM::threadPool()->start(closeDBJob, CLOSEDB); populateDirModel(); populateGlossary(); TM::threadPool()->waitForDone(500);//more safety TM::DBFilesModel::instance()->openDB(projectID(), TM::Undefined, true); } QString Project::absolutePath(const QString& possiblyRelPath) const { if (QFileInfo(possiblyRelPath).isRelative()) return QDir::cleanPath(m_projectDir % QLatin1Char('/') % possiblyRelPath); return possiblyRelPath; } void Project::populateDirModel() { if (Q_UNLIKELY(m_path.isEmpty() || !QFileInfo::exists(poDir()))) return; QUrl potUrl; if (QFileInfo::exists(potDir())) potUrl = QUrl::fromLocalFile(potDir()); model()->setUrl(QUrl::fromLocalFile(poDir()), potUrl); } void Project::populateGlossary() { m_glossary->load(glossaryPath()); } GlossaryNS::GlossaryWindow* Project::showGlossary() { return defineNewTerm(); } GlossaryNS::GlossaryWindow* Project::defineNewTerm(QString en, QString target) { if (!SettingsController::instance()->ensureProjectIsLoaded()) return 0; if (!m_glossaryWindow) m_glossaryWindow = new GlossaryNS::GlossaryWindow(SettingsController::instance()->mainWindowPtr()); m_glossaryWindow->show(); m_glossaryWindow->activateWindow(); if (!en.isEmpty() || !target.isEmpty()) m_glossaryWindow->newTermEntry(en, target); return m_glossaryWindow; } bool Project::queryCloseForAuxiliaryWindows() { if (m_glossaryWindow && m_glossaryWindow->isVisible()) return m_glossaryWindow->queryClose(); return true; } bool Project::isTmSupported() const { QStringList drivers = QSqlDatabase::drivers(); return drivers.contains(QLatin1String("QSQLITE")); } void Project::showTMManager() { if (!m_tmManagerWindow) { if (!isTmSupported()) { KMessageBox::information(nullptr, i18n("TM facility requires SQLite Qt module."), i18n("No SQLite module available")); return; } m_tmManagerWindow = new TM::TMManagerWin(SettingsController::instance()->mainWindowPtr()); } m_tmManagerWindow->show(); m_tmManagerWindow->activateWindow(); } bool Project::isFileMissing(const QString& filePath) const { if (!QFile::exists(filePath) && isLoaded()) { //check if we are opening template QString newPath = filePath; newPath.replace(poDir(), potDir()); if (!QFile::exists(newPath) && !QFile::exists(newPath += 't')) { return true; } } return false; } void Project::save() { m_localConfig->setFirstRun(false); ProjectBase::setTargetLangCode(langCode()); ProjectBase::save(); m_localConfig->save(); } ProjectModel* Project::model() { if (Q_UNLIKELY(!m_model)) m_model = new ProjectModel(this); return m_model; } void Project::setDefaults() { ProjectBase::setDefaults(); setLangCode(QLocale::system().name()); } void Project::init(const QString& path, const QString& kind, const QString& id, const QString& sourceLang, const QString& targetLang) { setDefaults(); bool stop = false; while (true) { setKind(kind); setSourceLangCode(sourceLang); setLangCode(targetLang); setProjectID(id); if (stop) break; else { load(path); stop = true; } } save(); } static void fillFilePathsRecursive(const QDir& dir, QMultiMap& sourceFilePaths) { QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) fillFilePathsRecursive(QDir(dir.filePath(subDirs.at(i))), sourceFilePaths); static QStringList filters = QStringList(QStringLiteral("*.cpp")) << QStringLiteral("*.c") << QStringLiteral("*.cc") << QStringLiteral("*.mm") << QStringLiteral("*.ui") << QStringLiteral("*rc"); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); QByteArray absDirPath = dir.absolutePath().toUtf8(); absDirPath.squeeze(); while (--i >= 0) { //qCDebug(LOKALIZE_LOG)<sourceFilePathsAreReady(); } protected: bool doKill() override; private: QString m_folderName; }; SourceFilesSearchJob::SourceFilesSearchJob(const QString& folderName, QObject* parent) : KJob(parent) , m_folderName(folderName) { setCapabilities(KJob::Killable); } bool SourceFilesSearchJob::doKill() { //TODO return true; } class FillSourceFilePathsJob: public QRunnable { public: explicit FillSourceFilePathsJob(const QDir& dir, SourceFilesSearchJob* j): startingDir(dir), kj(j) {} protected: void run() override { QMultiMap sourceFilePaths; fillFilePathsRecursive(startingDir, sourceFilePaths); Project::instance()->m_sourceFilePaths = sourceFilePaths; QTimer::singleShot(0, kj, &SourceFilesSearchJob::finish); } public: QDir startingDir; SourceFilesSearchJob* kj; }; void SourceFilesSearchJob::start() { QThreadPool::globalInstance()->start(new FillSourceFilePathsJob(QDir(m_folderName), this)); emit description(this, i18n("Scanning folders with source files"), qMakePair(i18n("Editor"), m_folderName)); } const QMultiMap& Project::sourceFilePaths() { if (m_sourceFilePaths.isEmpty()) { QDir dir(local()->sourceDir()); if (dir.exists()) { SourceFilesSearchJob* metaJob = new SourceFilesSearchJob(local()->sourceDir()); KIO::getJobTracker()->registerJob(metaJob); metaJob->start(); //KNotification* notification=new KNotification("SourceFileScan", 0); //notification->setText( i18nc("@info","Please wait while %1 is being scanned for source files.", local()->sourceDir()) ); //notification->sendEvent(); } } return m_sourceFilePaths; } #include #include #include "languagelistmodel.h" void Project::projectOdfCreate() { QString odf2xliff = QStringLiteral("odf2xliff"); if (QProcess::execute(odf2xliff, QStringList(QLatin1String("--version"))) == -2) { KMessageBox::error(SettingsController::instance()->mainWindowPtr(), i18n("Install translate-toolkit package and retry")); return; } QString odfPath = QFileDialog::getOpenFileName(SettingsController::instance()->mainWindowPtr(), QString(), QDir::homePath()/*_catalog->url().directory()*/, i18n("OpenDocument files (*.odt *.ods)")/*"text/x-lokalize-project"*/); if (odfPath.isEmpty()) return; QString targetLangCode = getTargetLangCode(QString(), true); QFileInfo fi(odfPath); QString trFolderName = i18nc("project folder name. %2 is targetLangCode", "%1 %2 Translation", fi.baseName(), targetLangCode); fi.absoluteDir().mkdir(trFolderName); QStringList args(odfPath); args.append(fi.absoluteDir().absoluteFilePath(trFolderName) % '/' % fi.baseName() % QLatin1String(".xlf")); qCDebug(LOKALIZE_LOG) << args; QProcess::execute(odf2xliff, args); if (!QFile::exists(args.at(1))) return; emit closed(); Project::instance()->load(fi.absoluteDir().absoluteFilePath(trFolderName) + QLatin1String("/index.lokalize"), targetLangCode, fi.baseName() % '-' % targetLangCode); emit fileOpenRequested(args.at(1), true); } diff --git a/src/project/project.h b/src/project/project.h index 783fc8d..14b3e23 100644 --- a/src/project/project.h +++ b/src/project/project.h @@ -1,216 +1,217 @@ /* **************************************************************************** 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 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(); } 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; //cache QString m_projectDir; }; #endif diff --git a/src/project/projectmodel.cpp b/src/project/projectmodel.cpp index 461d492..8f41c39 100644 --- a/src/project/projectmodel.cpp +++ b/src/project/projectmodel.cpp @@ -1,1441 +1,1442 @@ /* **************************************************************************** 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 + 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 "projectmodel.h" #include "lokalize_debug.h" #include "project.h" #include "poextractor.h" #include "xliffextractor.h" #include #include #include #include #include #include #include #include #include #include #include #include static int nodeCounter = 0; ProjectModel::ProjectModel(QObject *parent) : QAbstractItemModel(parent) , m_poModel(this) , m_potModel(this) , m_rootNode(ProjectNode(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.translated = -1; m_rootNode.translated_reviewer = -1; m_rootNode.translated_approver = -1; m_rootNode.untranslated = -1; m_rootNode.fuzzy = -1; m_rootNode.fuzzy_reviewer = -1; m_rootNode.fuzzy_approver = -1; m_rootNode.invalid_file = false; 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 qSort(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 { switch (role) { case Qt::TextAlignmentRole: { switch (section) { // 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 TotalCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); case TranslatedCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); case FuzzyCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); case UntranslatedCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); case IncompleteCount: return QVariant(Qt::AlignRight | Qt::AlignAbsolute); default: return QVariant(Qt::AlignLeft); } } case Qt::DisplayRole: { switch (section) { case FileName: return i18nc("@title:column File name", "Name"); case Graph: return i18nc("@title:column Graphical representation of Translated/Fuzzy/Untranslated counts", "Graph"); case TotalCount: return i18nc("@title:column Number of entries", "Total"); case TranslatedCount: return i18nc("@title:column Number of entries", "Translated"); case FuzzyCount: return i18nc("@title:column Number of entries", "Not ready"); case UntranslatedCount: return i18nc("@title:column Number of entries", "Untranslated"); case IncompleteCount: return i18nc("@title:column Number of fuzzy or untranslated entries", "Incomplete"); case TranslationDate: return i18nc("@title:column", "Last Translation"); case SourceDate: return i18nc("@title:column", "Template Revision"); case LastTranslator: return i18nc("@title:column", "Last Translator"); default: return QVariant(); } } default: return QVariant(); } } Qt::ItemFlags ProjectModel::flags(const QModelIndex & index) const { if (index.column() == 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 ProjectModelColumns& column = (ProjectModelColumns)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 int translated = node->translatedAsPerRole(); const int fuzzy = node->fuzzyAsPerRole(); const int untranslated = node->untranslated; const bool invalid_file = node->invalid_file; const bool hasStats = translated != -1 || invalid_file; switch (role) { case Qt::TextAlignmentRole: return ProjectModel::headerData(column, Qt::Horizontal, role); // Use same alignment as header case Qt::DisplayRole: switch (column) { case FileName: return item.text(); case Graph: return hasStats ? QRect(translated, untranslated, fuzzy, 0) : QVariant(); case TotalCount: return hasStats ? (translated + untranslated + fuzzy) : QVariant(); case TranslatedCount: return hasStats ? translated : QVariant(); case FuzzyCount: return hasStats ? fuzzy : QVariant(); case UntranslatedCount: return hasStats ? untranslated : QVariant(); case IncompleteCount: return hasStats ? (untranslated + fuzzy) : QVariant(); case SourceDate: return node->sourceDate; case TranslationDate: return node->translationDate; case LastTranslator: return node->lastTranslator; default: return QVariant(); } case Qt::ToolTipRole: switch (column) { case FileName: return item.text(); default: return QVariant(); } case KDirModel::FileItemRole: return QVariant::fromValue(item); case Qt::DecorationRole: switch (column) { case FileName: 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(); default: 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())->untranslated << static_cast(index.internalPointer())->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(), 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, 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(), 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.untranslated > 0 && !done) m_doneTimer->start(2000); emit loadingFinished(); } emit totalsChanged(m_rootNode.fuzzyAsPerRole(), m_rootNode.translatedAsPerRole(), m_rootNode.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) , invalid_file(false) , translated(-1) , translated_reviewer(-1) , translated_approver(-1) , untranslated(-10) , fuzzy(-1) , fuzzy_reviewer(-10) , fuzzy_approver(-10) { ++nodeCounter; } ProjectModel::ProjectNode::~ProjectNode() { --nodeCounter; } void ProjectModel::ProjectNode::calculateDirStats() { fuzzy = 0; fuzzy_reviewer = 0; fuzzy_approver = 0; translated = 0; translated_reviewer = 0; translated_approver = 0; untranslated = 0; for (int pos = 0; pos < rows.count(); pos++) { ProjectNode* child = rows.at(pos); if (!child->invalid_file && child->translated != -1) { fuzzy += child->fuzzy; fuzzy_reviewer += child->fuzzy_reviewer; fuzzy_approver += child->fuzzy_approver; translated += child->translated; translated_reviewer += child->translated_reviewer; translated_approver += child->translated_approver; untranslated += child->untranslated; } } } void ProjectModel::ProjectNode::setFileStats(const FileMetaData& info) { invalid_file = info.invalid_file; translated = info.translated; translated_reviewer = info.translated_reviewer; translated_approver = info.translated_approver; untranslated = info.untranslated; fuzzy = info.fuzzy; fuzzy_reviewer = info.fuzzy_reviewer; fuzzy_approver = info.fuzzy_approver; lastTranslator = info.lastTranslator; sourceDate = info.sourceDate; translationDate = info.translationDate; } //BEGIN UpdateStatsJob //these are run in separate thread UpdateStatsJob::UpdateStatsJob(const QList &files, QObject*) : QRunnable() , m_files(files) , m_status(0) { setAutoDelete(false); } UpdateStatsJob::~UpdateStatsJob() { } static FileMetaData metaData(QString filePath) { FileMetaData m; if (filePath.endsWith(QLatin1String(".po")) || filePath.endsWith(QLatin1String(".pot"))) { POExtractor extractor; extractor.extract(filePath, m); } else if (filePath.endsWith(QLatin1String(".xlf")) || filePath.endsWith(QLatin1String(".xliff"))) { XliffExtractor extractor; extractor.extract(filePath, m); } else if (filePath.endsWith(QLatin1String(".ts"))) { //POExtractor extractor; //extractor.extract(filePath, m); } return m; } //#define NOMETAINFOCACHE #ifndef NOMETAINFOCACHE static void initDataBase(QSqlDatabase& db) { QSqlQuery queryMain(db); queryMain.exec(QStringLiteral("PRAGMA encoding = \"UTF-8\"")); queryMain.exec(QStringLiteral( "CREATE TABLE IF NOT EXISTS metadata (" "filepath INTEGER PRIMARY KEY ON CONFLICT REPLACE, "// AUTOINCREMENT," //"filepath TEXT UNIQUE ON CONFLICT REPLACE, " "metadata BLOB, "//XLIFF markup info, see catalog/catalogstring.h catalog/xliff/* "changedate INTEGER" ")")); //queryMain.exec("CREATE INDEX IF NOT EXISTS filepath_index ON metainfo ("filepath)"); } QDataStream &operator<<(QDataStream &s, const FileMetaData &d) { //Magic number s << (quint32)0xABC42BCA; //Version s << (qint32)1; s << d.translated; s << d.translated_approver; s << d.translated_reviewer; s << d.fuzzy; s << d.fuzzy_approver; s << d.fuzzy_reviewer; s << d.untranslated; s << d.lastTranslator; s << d.translationDate; s << d.sourceDate; s << d.invalid_file; return s; } QDataStream &operator>>(QDataStream &s, FileMetaData &d) { //Read the magic number qint32 version = 0; quint32 magic; s >> magic; if (magic == 0xABC42BCA) { //This is a valid magic number, we can expect a version number //Else it's the old format s >> version; s >> d.translated; } else { //Legacy format, the magic number was actually the translated count d.translated = magic; } s >> d.translated_approver; s >> d.translated_reviewer; s >> d.fuzzy; s >> d.fuzzy_approver; s >> d.fuzzy_reviewer; s >> d.untranslated; s >> d.lastTranslator; s >> d.translationDate; s >> d.sourceDate; if (version >= 1) { s >> d.invalid_file; } return s; } #endif static FileMetaData cachedMetaData(const KFileItem& file) { if (file.isNull() || file.isDir()) return FileMetaData(); #ifdef NOMETAINFOCACHE return metaData(file.localPath()); #else QString dbName = QStringLiteral("metainfocache"); if (!QSqlDatabase::contains(dbName)) { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), dbName); db.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::DataLocation) % QLatin1Char('/') % dbName % QLatin1String(".sqlite")); if (Q_UNLIKELY(!db.open())) return metaData(file.localPath()); initDataBase(db); } QSqlDatabase db = QSqlDatabase::database(dbName); if (!db.isOpen()) return metaData(file.localPath()); QByteArray result; QSqlQuery queryCache(db); queryCache.prepare(QStringLiteral("SELECT * from metadata where filepath=?")); queryCache.bindValue(0, qHash(file.localPath())); queryCache.exec(); //not using file.time(KFileItem::ModificationTime) because it gives wrong result for files that have just been saved in editor if (queryCache.next() && QFileInfo(file.localPath()).lastModified() == queryCache.value(2).toDateTime()) { result = queryCache.value(1).toByteArray(); QDataStream stream(&result, QIODevice::ReadOnly); FileMetaData info; stream >> info; Q_ASSERT(info.translated == metaData(file.localPath()).translated); return info; } FileMetaData m = metaData(file.localPath()); QDataStream stream(&result, QIODevice::WriteOnly); //this is synced with ProjectModel::ProjectNode::setFileStats stream << m; QSqlQuery query(db); query.prepare(QStringLiteral("INSERT INTO metadata (filepath, metadata, changedate) " "VALUES (?, ?, ?)")); query.bindValue(0, qHash(file.localPath())); query.bindValue(1, result); query.bindValue(2, QFileInfo(file.localPath()).lastModified()); if (Q_UNLIKELY(!query.exec())) qCWarning(LOKALIZE_LOG) << "metainfo cache acquiring error: " << query.lastError().text(); return m; #endif } void UpdateStatsJob::run() { #ifndef NOMETAINFOCACHE QString dbName = QStringLiteral("metainfocache"); bool ok = QSqlDatabase::contains(dbName); if (ok) { QSqlDatabase db = QSqlDatabase::database(dbName); QSqlQuery queryBegin(QStringLiteral("BEGIN"), db); } #endif m_info.reserve(m_files.count()); for (int pos = 0; pos < m_files.count(); pos++) { if (m_status != 0) break; m_info.append(cachedMetaData(m_files.at(pos))); } #ifndef NOMETAINFOCACHE if (ok) { QSqlDatabase db = QSqlDatabase::database(dbName); { //braces are needed to avoid resource leak on close QSqlQuery queryEnd(QStringLiteral("END"), db); } db.close(); db.open(); } #endif emit done(this); } void UpdateStatsJob::setStatus(int status) { m_status = status; } //END UpdateStatsJob diff --git a/src/project/projectmodel.h b/src/project/projectmodel.h index 49fd4cb..d2c3641 100644 --- a/src/project/projectmodel.h +++ b/src/project/projectmodel.h @@ -1,298 +1,299 @@ /* **************************************************************************** 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 + 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 PROJECTMODEL_H #define PROJECTMODEL_H #include #include #include #include #include #include "project.h" #include "projectlocal.h" class QTimer; class QThreadPool; class UpdateStatsJob; struct FileMetaData { bool invalid_file; int translated; int translated_reviewer; int translated_approver; int untranslated; int fuzzy; int fuzzy_reviewer; int fuzzy_approver; QString lastTranslator; QString sourceDate; QString translationDate; QString filePath; FileMetaData() : invalid_file(false), translated(0), translated_reviewer(0), translated_approver(0), untranslated(0) , fuzzy(0), fuzzy_reviewer(0), fuzzy_approver(0) {} }; /** * 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(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 translated; case ProjectLocal::Reviewer: return translated_reviewer; case ProjectLocal::Approver: return translated_approver; } return -1; } int fuzzyAsPerRole() const { switch (Project::local()->role()) { case ProjectLocal::Translator: case ProjectLocal::Undefined: return fuzzy; case ProjectLocal::Reviewer: return fuzzy_reviewer; case ProjectLocal::Approver: return fuzzy_approver; } return -1; } 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; bool invalid_file; int translated; int translated_reviewer; int translated_approver; int untranslated; int fuzzy; int fuzzy_reviewer; int fuzzy_approver; QString sourceDate; QString lastTranslator; QString translationDate; }; public: enum ProjectModelColumns { FileName, Graph, TotalCount, TranslatedCount, FuzzyCount, UntranslatedCount, IncompleteCount, SourceDate, TranslationDate, LastTranslator, 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; }; class UpdateStatsJob: public QObject, public QRunnable { Q_OBJECT public: explicit UpdateStatsJob(const QList &files, QObject* owner = nullptr); ~UpdateStatsJob() override; int priority()const { return 35; //SEE jobs.h } void setStatus(int status); QList m_files; QList m_info; volatile int m_status; // 0 = running; -1 = cancel; -2 = abort protected: void run() override; signals: void done(UpdateStatsJob*); }; #endif diff --git a/src/project/projecttab.cpp b/src/project/projecttab.cpp index 61b6c3b..d1816d6 100644 --- a/src/project/projecttab.cpp +++ b/src/project/projecttab.cpp @@ -1,470 +1,471 @@ /* **************************************************************************** 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 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("http://community.kde.org/Get_Involved"), QLatin1String("http://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()); } ProjectTab::~ProjectTab() { //qCWarning(LOKALIZE_LOG)<<"destroyed"; } void ProjectTab::showRealProjectOverview() { m_stackedLayout->setCurrentIndex(1); } void ProjectTab::toggleTranslatedFiles() { m_browser->toggleTranslatedFiles(); } QString ProjectTab::currentFilePath() { return Project::instance()->path(); } void ProjectTab::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void ProjectTab::setFilterRegExp() { QString newPattern = m_filterEdit->text(); if (m_browser->proxyModel()->filterRegExp().pattern() == newPattern) return; m_browser->proxyModel()->setFilterRegExp(newPattern); if (newPattern.size() > 2) m_browser->expandItems(); } void ProjectTab::contextMenuEvent(QContextMenuEvent *event) { QMenu* menu = new QMenu(this); connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); if (m_browser->selectedItems().size() > 1 || (m_browser->selectedItems().size() == 1 && !m_browser->currentIsTranslationFile())) { menu->addAction(i18nc("@action:inmenu", "Open selected files"), this, &ProjectTab::openFile); menu->addSeparator(); } else if (m_browser->currentIsTranslationFile()) { menu->addAction(i18nc("@action:inmenu", "Open"), this, &ProjectTab::openFile); menu->addSeparator(); } /*menu.addAction(i18nc("@action:inmenu","Find in files"),this,&ProjectTab::findInFiles); menu.addAction(i18nc("@action:inmenu","Replace in files"),this,&ProjectTab::replaceInFiles); menu.addAction(i18nc("@action:inmenu","Spellcheck files"),this,&ProjectTab::spellcheckFiles); menu.addSeparator(); menu->addAction(i18nc("@action:inmenu","Get statistics for subfolders"),m_browser,&ProjectTab::expandItems); */ menu->addAction(i18nc("@action:inmenu", "Add to translation memory"), this, &ProjectTab::scanFilesToTM); menu->addAction(i18nc("@action:inmenu", "Search in files"), this, &ProjectTab::searchInFiles); if (Settings::self()->pologyEnabled()) { menu->addAction(i18nc("@action:inmenu", "Launch Pology on files"), this, &ProjectTab::pologyOnFiles); } if (QDir(Project::instance()->templatesRoot()).exists()) menu->addAction(i18nc("@action:inmenu", "Search in files (including templates)"), this, &ProjectTab::searchInFilesInclTempl); // else if (Project::instance()->model()->hasChildren(/*m_proxyModel->mapToSource(*/(m_browser->currentIndex())) // ) // { // menu.addSeparator(); // menu.addAction(i18n("Force Scanning"),this,&ProjectTab::slotForceStats); // // } menu->popup(event->globalPos()); } void ProjectTab::scanFilesToTM() { TM::scanRecursive(m_browser->selectedItems(), Project::instance()->projectID()); } void ProjectTab::searchInFiles(bool templ) { QStringList files = m_browser->selectedItems(); if (!templ) { QString templatesRoot = Project::instance()->templatesRoot(); int i = files.size(); while (--i >= 0) { if (files.at(i).startsWith(templatesRoot)) files.removeAt(i); } } emit searchRequested(files); } void ProjectTab::pologyOnFiles() { if (!m_pologyProcessInProgress) { QStringList files = m_browser->selectedItems(); QString templatesRoot = Project::instance()->templatesRoot(); QString filesAsString; int i = files.size(); while (--i >= 0) { if (files.at(i).endsWith(QStringLiteral(".po"))) filesAsString += QStringLiteral("\"") + files.at(i) + QStringLiteral("\" "); } QString command = Settings::self()->pologyCommandFile().replace(QStringLiteral("%f"), filesAsString); m_pologyProcess = new KProcess; m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); qCWarning(LOKALIZE_LOG) << "Launching pology command: " << command; connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &ProjectTab::pologyHasFinished); m_pologyProcess->setShellCommand(command); m_pologyProcessInProgress = true; m_pologyProcess->start(); } else { KMessageBox::error(this, i18n("A Pology check is already in progress."), i18n("Pology error")); } } void ProjectTab::pologyHasFinished(int exitCode, QProcess::ExitStatus exitStatus) { const QString pologyError = m_pologyProcess->readAllStandardError(); if (exitStatus == QProcess::CrashExit) { KMessageBox::error(this, i18n("The Pology check has crashed unexpectedly:\n%1", pologyError), i18n("Pology error")); } else if (exitCode == 0) { KMessageBox::information(this, i18n("The Pology check has succeeded"), i18n("Pology success")); } else { KMessageBox::error(this, i18n("The Pology check has returned an error:\n%1", pologyError), i18n("Pology error")); } m_pologyProcess->deleteLater(); m_pologyProcessInProgress = false; } void ProjectTab::searchInFilesInclTempl() { searchInFiles(true); } void ProjectTab::openFile() { QStringList files = m_browser->selectedItems(); int i = files.size(); 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 818c8ea..54bc979 100644 --- a/src/project/projecttab.h +++ b/src/project/projecttab.h @@ -1,131 +1,132 @@ /* **************************************************************************** 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; 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(); //Q_SCRIPTABLE bool isShown() const; private slots: void setFilterRegExp(); void setFocus(); void scanFilesToTM(); void pologyOnFiles(); 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 20957cb..5e05626 100644 --- a/src/project/projectwidget.cpp +++ b/src/project/projectwidget.cpp @@ -1,550 +1,551 @@ /* **************************************************************************** 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; 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 (index.column() != ProjectModel::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)); } 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 (left.column()) { case ProjectModel::FileName: return collator.compare(leftFileItem.name(), rightFileItem.name()) < 0; case ProjectModel::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::LastTranslator: case ProjectModel::SourceDate: case ProjectModel::TranslationDate: return collator.compare(projectModel->data(left).toString(), projectModel->data(right).toString()) < 0; case ProjectModel::TotalCount: case ProjectModel::TranslatedCount: case ProjectModel::UntranslatedCount: case ProjectModel::IncompleteCount: case ProjectModel::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 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(idx.child(j, 0))); if (childItem.isDir()) recursiveAdd(list, idx.child(j, 0)); 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.child((direction == 1) ? 0 : (m_proxyModel->rowCount(index) - 1), index.column()); 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; } diff --git a/src/project/projectwidget.h b/src/project/projectwidget.h index 9b5510a..b6cf30f 100644 --- a/src/project/projectwidget.h +++ b/src/project/projectwidget.h @@ -1,90 +1,91 @@ /* **************************************************************************** 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 PROJECTWIDGET_H #define PROJECTWIDGET_H #include #include "projectmodel.h" class SortFilterProxyModel; class QSortFilterProxyModel; /** * This class is considered a 'view', * and ProjectWindow + ProjectView are its controllers * the data is project-wide KDirModel based ProjectModel */ class ProjectWidget: public QTreeView { Q_OBJECT public: explicit ProjectWidget(QWidget* parent); ~ProjectWidget(); bool setCurrentItem(const QString&); QString currentItem() const; QStringList selectedItems() const; bool currentIsTranslationFile() const; QSortFilterProxyModel* proxyModel(); void expandItems(const QModelIndex& parent = QModelIndex()); void gotoPrevFuzzyUntr(); void gotoNextFuzzyUntr(); void gotoPrevFuzzy(); void gotoNextFuzzy(); void gotoPrevUntranslated(); void gotoNextUntranslated(); void gotoPrevTemplateOnly(); void gotoNextTemplateOnly(); void gotoPrevTransOnly(); void gotoNextTransOnly(); void toggleTranslatedFiles(); signals: void fileOpenRequested(const QString&, const bool setAsActive); void newWindowOpenRequested(const QUrl&); private slots: void slotItemActivated(const QModelIndex&); void modelAboutToReload(); void modelReloaded(); private: enum gotoIndexResult {gotoIndex_end = -1, gotoIndex_notfound = 0, gotoIndex_found = 1}; bool gotoIndexCheck(const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role); QModelIndex gotoIndexPrevNext(const QModelIndex& currentIndex, int direction) const; gotoIndexResult gotoIndexFind(const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction); gotoIndexResult gotoIndex(const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction); void recursiveAdd(QStringList& list, const QModelIndex& idx) const; SortFilterProxyModel* m_proxyModel; QString m_currentItemPathBeforeReload; }; #endif diff --git a/src/project/xliffextractor.cpp b/src/project/xliffextractor.cpp index b80aebc..a57e337 100644 --- a/src/project/xliffextractor.cpp +++ b/src/project/xliffextractor.cpp @@ -1,180 +1,181 @@ /* XLIFF translation file analyzer Copyright (C) 2011 Albert Astals Cid Copyright (C) 2015 Nick Shaforostoff + 2018-2019 by Simon Depiets This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "xliffextractor.h" #include "lokalize_debug.h" #include "catalog.h" #include #include #include XliffExtractor::XliffExtractor() { } class XliffHandler: public QXmlDefaultHandler { public: XliffHandler() : total(0) , untranslated(0) , fuzzy(0) , fuzzy_reviewer(0) , fuzzy_approver(0) , currentEntryFuzzy(false) , currentEntryFuzzy_reviewer(false) , currentEntryFuzzy_approver(false) , charCount(0) {} bool startElement(const QString& namespaceURI, const QString& localName, const QString& qName, const QXmlAttributes& atts) override; bool endElement(const QString& namespaceURI, const QString& localName, const QString& qName) override; bool characters(const QString&) override; //void endAnalysis(bool complete); int total; int untranslated; int fuzzy; int fuzzy_reviewer; int fuzzy_approver; QDate lastDate; QString lastTranslator; QString lastTranslator_fallback; QString lastDateString_fallback; private: bool currentEntryFuzzy; bool currentEntryFuzzy_reviewer; bool currentEntryFuzzy_approver; int charCount; }; extern const QString xliff_states[]; TargetState stringToState(const QString& state); bool XliffHandler::startElement(const QString&, const QString& localName, const QString&, const QXmlAttributes& atts) { //if (fileType == Unknown) // fileType = strcmp(localname, "xliff") ? Other : XLF; if (localName == QLatin1String("source")) total++; else if (localName == QLatin1String("target")) { charCount = 0; currentEntryFuzzy = currentEntryFuzzy_reviewer = currentEntryFuzzy_approver = false; if (atts.value(QLatin1String("approved")) != QLatin1String("yes")) { QString state = atts.value(QLatin1String("state")); if (state.length()) { TargetState tstate = stringToState(state); currentEntryFuzzy = !::isApproved(tstate, ProjectLocal::Translator); currentEntryFuzzy_reviewer = !::isApproved(tstate, ProjectLocal::Reviewer); currentEntryFuzzy_approver = !::isApproved(tstate, ProjectLocal::Approver); } } } else if (localName == QLatin1String("phase")) { QString contactNameString = atts.value(QLatin1String("contact-name")); QString contactEmailString = atts.value(QLatin1String("contact-email")); QString dateString = atts.value(QLatin1String("date")); QString currentLastTranslator; if (contactNameString.length() && contactEmailString.length()) currentLastTranslator = contactNameString % " <" % contactEmailString % ">"; else if (contactNameString.length()) currentLastTranslator = contactNameString; else if (contactEmailString.length()) currentLastTranslator = contactEmailString; if (currentLastTranslator.length()) lastTranslator_fallback = currentLastTranslator; if (dateString.length()) { lastDateString_fallback = dateString; const QDate thisDate = QDate::fromString(dateString, Qt::ISODate); if (lastDate.isNull() || thisDate >= lastDate) { // >= Assuming the last one in the file is the real last one lastDate = thisDate; lastTranslator = currentLastTranslator; } } } return true; } bool XliffHandler::endElement(const QString&, const QString& localName, const QString&) { if (localName == QLatin1String("target")) { if (!charCount) { ++untranslated; } else if (currentEntryFuzzy) { ++fuzzy; ++fuzzy_reviewer; ++fuzzy_approver; } else if (currentEntryFuzzy_reviewer) { ++fuzzy_reviewer; ++fuzzy_approver; } else if (currentEntryFuzzy_approver) { ++fuzzy_approver; } } return true; } bool XliffHandler::characters(const QString& ch) { charCount += ch.length(); return true; } void XliffExtractor::extract(const QString& filePath, FileMetaData& m) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; QXmlInputSource source(&file); QXmlSimpleReader xmlReader; XliffHandler handler; xmlReader.setContentHandler(&handler); bool ok = xmlReader.parse(source); if (!ok) qCDebug(LOKALIZE_LOG) << "Parsing failed."; //TODO WordCount m.fuzzy = handler.fuzzy; m.translated = handler.total - handler.untranslated - handler.fuzzy; m.untranslated = handler.untranslated; m.filePath = filePath; //qCDebug(LOKALIZE_LOG)<<"parsed"<= 0 && m.untranslated >= 0 && handler.total >= 0); m.translated_approver = handler.total - handler.untranslated - handler.fuzzy_approver; m.translated_reviewer = handler.total - handler.untranslated - handler.fuzzy_reviewer; m.fuzzy_approver = handler.fuzzy_approver; m.fuzzy_reviewer = handler.fuzzy_reviewer; m.lastTranslator = handler.lastTranslator.length() ? handler.lastTranslator : handler.lastTranslator_fallback; m.translationDate = handler.lastDate.isValid() ? handler.lastDate.toString(Qt::ISODate) : handler.lastDateString_fallback; } diff --git a/src/project/xliffextractor.h b/src/project/xliffextractor.h index 79a6cde..813c65f 100644 --- a/src/project/xliffextractor.h +++ b/src/project/xliffextractor.h @@ -1,37 +1,38 @@ /* XLIFF translation file analyzer Copyright (C) 2011 Albert Astals Cid Copyright (C) 2015 Nick Shaforostoff + 2018-2019 by Simon Depiets This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef XLIFFEXTRACTOR_H #define XLIFFEXTRACTOR_H #include "projectmodel.h" class XliffExtractor { public: XliffExtractor(); void extract(const QString& filePath, FileMetaData& data); }; #endif // PLAINTEXTEXTRACTOR_H diff --git a/src/syntaxhighlighter.cpp b/src/syntaxhighlighter.cpp index cc832b0..f99dd49 100644 --- a/src/syntaxhighlighter.cpp +++ b/src/syntaxhighlighter.cpp @@ -1,278 +1,279 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "syntaxhighlighter.h" #include "lokalize_debug.h" #include "project.h" #include "prefs_lokalize.h" #include "prefs.h" #include #include #include #include #define STATE_NORMAL 0 #define STATE_TAG 1 #define NUM_OF_RULES 5 SyntaxHighlighter::SyntaxHighlighter(QTextEdit *parent) : Sonnet::Highlighter(parent) , tagBrush(KColorScheme::View, KColorScheme::VisitedText) , m_approved(true) // , fromDocbook(docbook) { highlightingRules.reserve(NUM_OF_RULES); HighlightingRule rule; //rule.format.setFontItalic(true); // tagFormat.setForeground(tagBrush.brush(QApplication::palette())); setAutomatic(false); tagFormat.setForeground(tagBrush.brush(QApplication::palette())); //QTextCharFormat format; //tagFormat.setForeground(Qt::darkBlue); // if (!docbook) //support multiline tags // { // rule.format = tagFormat; // rule.pattern = QRegExp("<.+>"); // rule.pattern.setMinimal(true); // highlightingRules.append(rule); // } //entity rule.format.setForeground(Qt::darkMagenta); rule.pattern = QRegExp(QStringLiteral("(&[A-Za-z_:][A-Za-z0-9_\\.:-]*;)")); highlightingRules.append(rule); QString accel = Project::instance()->accel(); if (!accel.isEmpty()) { rule.format.setForeground(Qt::darkMagenta); rule.pattern = QRegExp(accel); highlightingRules.append(rule); } //\n \t \" rule.format.setForeground(Qt::darkGreen); rule.pattern = QRegExp(QStringLiteral("(\\\\[abfnrtv'\?\\\\])|(\\\\\\d+)|(\\\\x[\\dabcdef]+)")); highlightingRules.append(rule); //spaces settingsChanged(); connect(SettingsController::instance(), &SettingsController::generalSettingsChanged, this, &SyntaxHighlighter::settingsChanged); } void SyntaxHighlighter::settingsChanged() { QRegExp re(" +$|^ +|.?" % QChar(0x0000AD) % ".?"); //soft hyphen if (Settings::highlightSpaces() && highlightingRules.last().pattern != re) { HighlightingRule rule; rule.format.clearForeground(); KColorScheme colorScheme(QPalette::Normal); //nbsp //rule.format.setBackground(colorScheme.background(KColorScheme::NegativeBackground)); rule.format.setBackground(colorScheme.foreground(KColorScheme::InactiveText)); rule.format.setFontLetterSpacing(200); rule.pattern = QRegExp(QChar(0x00a0U), Qt::CaseSensitive, QRegExp::FixedString); highlightingRules.append(rule); //usual spaces at the end rule.format.setFontLetterSpacing(100); rule.format.setBackground(colorScheme.background(KColorScheme::ActiveBackground)); rule.pattern = re; highlightingRules.append(rule); rehighlight(); } else if (!Settings::highlightSpaces() && highlightingRules.last().pattern == re) { highlightingRules.resize(highlightingRules.size() - 2); rehighlight(); } } /* void SyntaxHighlighter::setFuzzyState(bool fuzzy) { return; int i=NUM_OF_RULES; while(--i>=0) highlightingRules[i].format.setFontItalic(fuzzy); tagFormat.setFontItalic(fuzzy); }*/ void SyntaxHighlighter::highlightBlock(const QString &text) { int currentBlockState = STATE_NORMAL; QTextCharFormat f; f.setFontItalic(!m_approved); setFormat(0, text.length(), f); tagFormat.setFontItalic(!m_approved); //if (fromDocbook) { int startIndex = STATE_NORMAL; if (previousBlockState() != STATE_TAG) startIndex = text.indexOf('<'); while (startIndex >= 0) { int endIndex = text.indexOf('>', startIndex); int commentLength; if (endIndex == -1) { currentBlockState = STATE_TAG; commentLength = text.length() - startIndex; } else { commentLength = endIndex - startIndex + 1/*+ commentEndExpression.matchedLength()*/; } setFormat(startIndex, commentLength, tagFormat); startIndex = text.indexOf('<', startIndex + commentLength); } } foreach (const HighlightingRule &rule, highlightingRules) { QRegExp expression(rule.pattern); int index = expression.indexIn(text); while (index >= 0) { int length = expression.matchedLength(); QTextCharFormat f = rule.format; f.setFontItalic(!m_approved); setFormat(index, length, f); index = expression.indexIn(text, index + length); } } if (spellCheckerFound()) Sonnet::Highlighter::highlightBlock(text); // Resets current block state setCurrentBlockState(currentBlockState); } #if 0 void SyntaxHighlighter::setFormatRetainingUnderlines(int start, int count, QTextCharFormat f) { QVector underLines(count); for (int i = 0; i < count; ++i) underLines[i] = format(start + i).fontUnderline(); setFormat(start, count, f); f.setFontUnderline(true); int prevStart = -1; bool isPrevUnderLined = false; for (int i = 0; i < count; ++i) { if (!underLines.at(i) && prevStart != -1) setFormat(start + isPrevUnderLined, i - prevStart, f); else if (underLines.at(i) && !isPrevUnderLined) prevStart = i; isPrevUnderLined = underLines.at(i); } } #endif void SyntaxHighlighter::setMisspelled(int start, int count) { const Project& project = *Project::instance(); const QString text = currentBlock().text(); QString word = text.mid(start, count); if (m_sourceString.contains(word) && project.targetLangCode().leftRef(2) != project.sourceLangCode().leftRef(2)) return; const QString accel = project.accel(); if (!isWordMisspelled(word.remove(accel))) return; count = word.length(); //safety bool smthPreceeding = (start > 0) && (accel.endsWith(text.at(start - 1)) || text.at(start - 1) == QChar(0x0000AD) //soft hyphen ); //HACK. Needs Sonnet API redesign (KDE 5) if (smthPreceeding) { qCWarning(LOKALIZE_LOG) << "ampersand is in the way. word len:" << count; int realStart = text.lastIndexOf(QRegExp("\\b"), start - 2); if (realStart == -1) realStart = 0; QString t = text.mid(realStart, count + start - realStart); t.remove(accel); t.remove(QChar(0x0000AD)); if (!isWordMisspelled(t)) return; } bool smthAfter = (start + count + 1 < text.size()) && (accel.startsWith(text.at(start + count)) || text.at(start + count) == QChar(0x0000AD) //soft hyphen ); if (smthAfter) { qCWarning(LOKALIZE_LOG) << "smthAfter. ampersand is in the way. word len:" << count; int realEnd = text.indexOf(QRegExp(QStringLiteral("\\b")), start + count + 2); if (realEnd == -1) realEnd = text.size(); QString t = text.mid(start, realEnd - start); t.remove(accel); t.remove(QChar(0x0000AD)); if (!isWordMisspelled(t)) return; } if (count && format(start) == tagFormat) return; for (int i = 0; i < count; ++i) { QTextCharFormat f(format(start + i)); f.setFontUnderline(true); f.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); f.setUnderlineColor(Qt::red); setFormat(start + i, 1, f); } } void SyntaxHighlighter::unsetMisspelled(int start, int count) { for (int i = 0; i < count; ++i) { QTextCharFormat f(format(start + i)); f.setFontUnderline(false); setFormat(start + i, 1, f); } } diff --git a/src/syntaxhighlighter.h b/src/syntaxhighlighter.h index f097526..94930c4 100644 --- a/src/syntaxhighlighter.h +++ b/src/syntaxhighlighter.h @@ -1,79 +1,80 @@ /* **************************************************************************** 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 HIGHLIGHTER_H #define HIGHLIGHTER_H #include #include #include #include #include class QTextDocument; class QTextEdit; class SyntaxHighlighter : public Sonnet::Highlighter { Q_OBJECT public: explicit SyntaxHighlighter(QTextEdit *parent); ~SyntaxHighlighter() override {} void setApprovementState(bool a) { m_approved = a; } void setSourceString(const QString& s) { m_sourceString = s; } protected: void highlightBlock(const QString &text) override; void setMisspelled(int start, int count) override; void unsetMisspelled(int start, int count) override; private slots: void settingsChanged(); // void setFormatRetainingUnderlines(int start, int count, QTextCharFormat format); private: struct HighlightingRule { QRegExp pattern; QTextCharFormat format; }; QVector highlightingRules; // bool fromDocbook; QTextCharFormat tagFormat; KStatefulBrush tagBrush; bool m_approved; QString m_sourceString; }; #endif diff --git a/src/tm/dbfilesmodel.cpp b/src/tm/dbfilesmodel.cpp index 4df1dfd..b6f9525 100644 --- a/src/tm/dbfilesmodel.cpp +++ b/src/tm/dbfilesmodel.cpp @@ -1,248 +1,249 @@ /* **************************************************************************** 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 "dbfilesmodel.h" #include "lokalize_debug.h" #include "jobs.h" #include "project.h" #include #include #include #include #include #if defined(Q_OS_WIN) && defined(QStringLiteral) #undef QStringLiteral #define QStringLiteral QLatin1String #endif using namespace TM; static QString tmFileExtension = QStringLiteral(TM_DATABASE_EXTENSION); static QString remoteTmExtension = QStringLiteral(REMOTETM_DATABASE_EXTENSION); DBFilesModel* DBFilesModel::_instance = 0; void DBFilesModel::cleanupDBFilesModel() { delete DBFilesModel::_instance; DBFilesModel::_instance = 0; } DBFilesModel* DBFilesModel::instance() { if (Q_UNLIKELY(_instance == 0)) { _instance = new DBFilesModel; qAddPostRoutine(DBFilesModel::cleanupDBFilesModel); } return _instance; } DBFilesModel::DBFilesModel() : QSortFilterProxyModel() , projectDB(0) , m_fileSystemModel(new QFileSystemModel(this)) , m_tmRootPath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)) { m_fileSystemModel->setNameFilters(QStringList(QStringLiteral("*." TM_DATABASE_EXTENSION))); m_fileSystemModel->setFilter(QDir::Files); m_fileSystemModel->setRootPath(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); setSourceModel(m_fileSystemModel); connect(this, &DBFilesModel::rowsInserted, this, &DBFilesModel::calcStats/*,Qt::QueuedConnection*/); connect(this, &DBFilesModel::dataChanged, this, &DBFilesModel::updateStats, Qt::QueuedConnection); m_timeSinceLastUpdate.start(); int count = rowCount(rootIndex()); if (count) calcStats(rootIndex(), 0, count - 1); openDB(QStringLiteral("default")); //behave when no project is loaded } DBFilesModel::~DBFilesModel() { delete projectDB; } bool DBFilesModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (source_parent != m_fileSystemModel->index(m_tmRootPath)) return true; const QString& fileName = m_fileSystemModel->index(source_row, 0, source_parent).data().toString(); return (fileName.endsWith(tmFileExtension) && !fileName.endsWith(QLatin1String("-journal.db"))) || fileName.endsWith(remoteTmExtension); } QModelIndex DBFilesModel::rootIndex() const { return mapFromSource(m_fileSystemModel->index(m_tmRootPath)); } QVariant DBFilesModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation); if (role != Qt::DisplayRole) return QVariant(); const char* const columns[] = { I18N_NOOP2("@title:column", "Name"), I18N_NOOP2("@title:column", "Source language"), I18N_NOOP2("@title:column", "Target language"), I18N_NOOP2("@title:column", "Pairs"), I18N_NOOP2("@title:column", "Unique original entries"), I18N_NOOP2("@title:column", "Unique translations") }; return i18nc("@title:column", columns[section]); } void DBFilesModel::openDB(const QString& name, DbType type, bool forceCurrentProjectConfig) { m_openingDbLock.lock(); if (m_openingDb.contains(name)) { //Database already being opened m_openingDbLock.unlock(); return; } m_openingDb.append(name); m_openingDbLock.unlock(); if (type == TM::Undefined) type = QFileInfo( QStandardPaths::writableLocation(QStandardPaths::DataLocation) % QLatin1Char('/') % name % QStringLiteral(REMOTETM_DATABASE_EXTENSION)).exists() ? TM::Remote : TM::Local; OpenDBJob* openDBJob = new OpenDBJob(name, type); if (forceCurrentProjectConfig) { openDBJob->m_setParams = true; openDBJob->m_tmConfig.markup = Project::instance()->markup(); openDBJob->m_tmConfig.accel = Project::instance()->accel(); openDBJob->m_tmConfig.sourceLangCode = Project::instance()->sourceLangCode(); openDBJob->m_tmConfig.targetLangCode = Project::instance()->targetLangCode(); } openDB(openDBJob); } void DBFilesModel::openDB(OpenDBJob* openDBJob) { connect(openDBJob, &OpenDBJob::done, this, &DBFilesModel::openJobDone); threadPool()->start(openDBJob, OPENDB); } void DBFilesModel::updateStats(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (m_timeSinceLastUpdate.elapsed() < 60000 || !topLeft.isValid() || !bottomRight.isValid()) return; qCDebug(LOKALIZE_LOG) << "DBFilesModel::updateStats() called"; calcStats(topLeft.parent(), topLeft.row(), bottomRight.row()); m_timeSinceLastUpdate.start(); } void DBFilesModel::calcStats(const QModelIndex& parent, int start, int end) { if (parent != rootIndex()) return; const QString& projectID = Project::instance()->projectID(); while (start <= end) { QModelIndex index = QSortFilterProxyModel::index(start++, 0, parent); QString res = index.data().toString(); if (Q_UNLIKELY(res == projectID && (!projectDB || data(*projectDB).toString() != projectID))) projectDB = new QPersistentModelIndex(index); //TODO if user switches the project // if (Q_LIKELY( QSqlDatabase::contains(res) )) // continue; openDB(res, DbType(index.data(FileNameRole).toString().endsWith(remoteTmExtension))); } } void DBFilesModel::openJobDone(OpenDBJob* j) { m_openingDbLock.lock(); m_openingDb.removeAll(j->m_dbName); m_openingDbLock.unlock(); j->deleteLater(); m_stats[j->m_dbName] = j->m_stat; m_configurations[j->m_dbName] = j->m_tmConfig; qCDebug(LOKALIZE_LOG) << j->m_dbName << j->m_tmConfig.targetLangCode; } void DBFilesModel::removeTM(QModelIndex index) { index = index.sibling(index.row(), 0); CloseDBJob* closeDBJob = new CloseDBJob(index.data().toString()); connect(closeDBJob, &CloseDBJob::done, this, &DBFilesModel::closeJobDone); threadPool()->start(closeDBJob, CLOSEDB); } void DBFilesModel::closeJobDone(CloseDBJob* j) { j->deleteLater(); QString filename = m_fileSystemModel->rootPath() % '/' % j->dbName() % tmFileExtension; qCWarning(LOKALIZE_LOG) << "removing file " << filename; QFile::remove(filename); } void DBFilesModel::updateProjectTmIndex() { if (projectDB && data(*projectDB).toString() != Project::instance()->projectID()) { delete projectDB; projectDB = 0; } } int DBFilesModel::columnCount(const QModelIndex&) const { return 4; //FIXME the lat two columns are not displayed even if 6 is returned } QVariant DBFilesModel::data(const QModelIndex& index, int role) const { if (role == Qt::DecorationRole) return QVariant(); if (role != Qt::DisplayRole && role != FileNameRole && role != NameRole && index.column() < 4) return QSortFilterProxyModel::data(index, role); QString res = QSortFilterProxyModel::data(index.sibling(index.row(), 0), QFileSystemModel::FileNameRole).toString(); if (role == FileNameRole) return res; if (res.endsWith(remoteTmExtension)) res.chop(remoteTmExtension.size()); else res.chop(tmFileExtension.size()); if (role == NameRole) return res; //qCDebug(LOKALIZE_LOG)< + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef DBFILESMODEL_H #define DBFILESMODEL_H #include "jobs.h" #include #include #include class QFileSystemModel; class QPersistentModelIndex; namespace TM { class OpenDBJob; class DBFilesModel: public QSortFilterProxyModel { Q_OBJECT public: enum Columns { Name = 0, SourceLang, TargetLang, Pairs, OriginalsCount, TranslationsCount, ColumnCount }; enum Rolse { FileNameRole = Qt::UserRole + 50, NameRole = Qt::UserRole + 51 }; DBFilesModel(); ~DBFilesModel() override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; Qt::ItemFlags flags(const QModelIndex&) const override { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex rootIndex() const; void removeTM(QModelIndex); //can be zero!!! QPersistentModelIndex* projectDBIndex()const { return projectDB; } void openDB(const QString& name, DbType type = Undefined, bool forceCurrentProjectConfig = false); void openDB(OpenDBJob*); static DBFilesModel* instance(); private: static DBFilesModel* _instance; static void cleanupDBFilesModel(); protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; public slots: void updateStats(const QModelIndex& topLeft, const QModelIndex& bottomRight); void calcStats(const QModelIndex& parent, int start, int end); void openJobDone(OpenDBJob*); void closeJobDone(CloseDBJob*); void updateProjectTmIndex(); private: mutable QPersistentModelIndex* projectDB; QFileSystemModel* m_fileSystemModel; QString m_tmRootPath; QTime m_timeSinceLastUpdate; QMap m_stats; public: QMap m_configurations; QList m_openingDb; QMutex m_openingDbLock; }; } #endif diff --git a/src/tm/jobs.cpp b/src/tm/jobs.cpp index 189b123..fa8ca1c 100644 --- a/src/tm/jobs.cpp +++ b/src/tm/jobs.cpp @@ -1,2143 +1,2144 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "jobs.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include "diff.h" #include "prefs_lokalize.h" #include "version.h" #include "stemming.h" #include #include #include #include #include #include #include #include #include #include #include using namespace TM; QThreadPool* TM::threadPool() { static QThreadPool* inst = new QThreadPool; return inst; } #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif #define TM_DELIMITER '\v' #define TM_SEPARATOR '\b' #define TM_NOTAPPROVED 0x04 static bool stop = false; void TM::cancelAllJobs() { stop = true; } static qlonglong newTMSourceEntryCount = 0; static qlonglong reusedTMSourceEntryCount = 0; /** * splits string into words, removing any markup * * TODO segmentation by sentences... **/ static void doSplit(QString& cleanEn, QStringList& words, QRegExp& rxClean1, const QString& accel ) { static QRegExp rxSplit(QStringLiteral("\\W+|\\d+")); if (!rxClean1.pattern().isEmpty()) cleanEn.replace(rxClean1, QStringLiteral(" ")); cleanEn.remove(accel); words = cleanEn.toLower().split(rxSplit, QString::SkipEmptyParts); if (words.size() > 4) { int i = 0; for (; i < words.size(); ++i) { if (words.at(i).size() < 4) words.removeAt(i--); else if (words.at(i).startsWith('t') && words.at(i).size() == 4) { if (words.at(i) == QLatin1String("then") || words.at(i) == QLatin1String("than") || words.at(i) == QLatin1String("that") || words.at(i) == QLatin1String("this") ) words.removeAt(i--); } } } } static qlonglong getFileId(const QString& path, QSqlDatabase& db) { QSqlQuery query1(db); QString escapedPath = path; escapedPath.replace(QLatin1Char('\''), QLatin1String("''")); QString pathExpr = QStringLiteral("path='") % escapedPath % '\''; if (path.isEmpty()) pathExpr = QStringLiteral("path ISNULL"); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "path='") % escapedPath % '\''))) qCWarning(LOKALIZE_LOG) << "select db error: " << query1.lastError().text(); if (Q_LIKELY(query1.next())) { //this is translation of en string that is already present in db qlonglong id = query1.value(0).toLongLong(); query1.clear(); return id; } query1.clear(); //nope, this is new file bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QString sql = QStringLiteral("INSERT INTO files (path) VALUES (?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.prepare(sql); query1.bindValue(0, path); if (Q_LIKELY(query1.exec())) return qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); else qCWarning(LOKALIZE_LOG) << "insert db error: " << query1.lastError().text(); return -1; } static void addToIndex(qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // insert word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") % words.at(j) % '\''))) qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); //we _have_ it bool weHaveIt = query1.next(); if (weHaveIt) { //just add new id QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' % sourceIdStr % ' ') || arr.startsWith(sourceIdStr + ' ') || arr.endsWith(' ' + sourceIdStr) || arr == sourceIdStr) return;//this string is already indexed query1.prepare(QStringLiteral("UPDATE words SET ") % field % QStringLiteral("=? WHERE word='") % words.at(j) % '\''); if (!arr.isEmpty()) arr += ' '; arr += sourceIdStr; query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 4: " << query1.lastError().text(); } else { query1.clear(); query1.prepare(QStringLiteral("INSERT INTO words (word, ids_short, ids_long) VALUES (?, ?, ?)")); QByteArray idsShort; QByteArray idsLong; if (isShort) idsShort = sourceIdStr; else idsLong = sourceIdStr; query1.bindValue(0, words.at(j)); query1.bindValue(1, idsShort); query1.bindValue(2, idsLong); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "insert error 2: " << query1.lastError().text() ; } } } /** * remove source string from index if there are no other * 'good' entries using it but the entry specified with mainId */ static void removeFromIndex(qlonglong mainId, qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); //BEGIN check //TM_NOTAPPROVED=4 if (Q_UNLIKELY(!query1.exec(U("SELECT count(*) FROM main, target_strings WHERE " "main.source=") % QString::number(sourceId) % U(" AND " "main.target=target_strings.id AND " "target_strings.target NOTNULL AND " "main.id!=") % QString::number(mainId) % U(" AND " "(main.bits&4)!=4")))) { qCWarning(LOKALIZE_LOG) << "select error 500: " << query1.lastError().text(); return; } bool exit = query1.next() && (query1.value(0).toLongLong() > 0); query1.clear(); if (exit) return; //END check bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // remove from record for the word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") % words.at(j) % '\''))) { qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); return; } if (!query1.next()) { qCWarning(LOKALIZE_LOG) << "exit here 1"; //we don't have record for the word, so nothing to remove query1.clear(); return; } QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' + sourceIdStr + ' ')) arr.replace(' ' + sourceIdStr + ' ', " "); else if (arr.startsWith(sourceIdStr + ' ')) arr.remove(0, sourceIdStr.size() + 1); else if (arr.endsWith(' ' + sourceIdStr)) arr.chop(sourceIdStr.size() + 1); else if (arr == sourceIdStr) arr.clear(); query1.prepare(U("UPDATE words " "SET ") % field % U("=? " "WHERE word='") % words.at(j) % '\''); query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 504: " << query1.lastError().text(); } } static bool doRemoveEntry(qlonglong mainId, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT source_strings.id, source_strings.source FROM source_strings, main WHERE " "source_strings.id=main.source AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong sourceId = query1.value(0).toLongLong(); const QString sourceString = query1.value(1).toString(); query1.clear(); if (Q_UNLIKELY(!query1.exec(U("SELECT target_strings.id FROM target_strings, main WHERE target_strings.id=main.target AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong targetId = query1.value(0).toLongLong(); query1.clear(); query1.exec(QStringLiteral("DELETE FROM main WHERE source=") + QString::number(sourceId) + QStringLiteral(" AND target=") + QString::number(targetId)); if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE source=") + QString::number(sourceId)) || !query1.next()) return false; const bool noSourceLeft = query1.value(0).toInt() == 0; query1.clear(); if (noSourceLeft) { removeFromIndex(mainId, sourceId, sourceString, rxClean1, accel, db); query1.exec(QStringLiteral("DELETE FROM source_strings WHERE id=") + QString::number(sourceId)); } if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE target=") + QString::number(targetId)) || ! query1.next()) return false; const bool noTargetLeft = query1.value(0).toInt() == 0; query1.clear(); if (noTargetLeft) query1.exec(QStringLiteral("DELETE FROM target_strings WHERE id=") + QString::number(targetId)); return true; } static bool doRemoveFile(const QString& filePath, QSqlDatabase& db) { qlonglong fileId = getFileId(filePath, db); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "id=") + QString::number(fileId)))) return false; if (!query1.next()) return false; query1.clear(); query1.exec(QStringLiteral("DELETE source_strings FROM source_strings, main WHERE source_strings.id = main.source AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE target_strings FROM target_strings, main WHERE target_strings.id = main.target AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE FROM main WHERE file = ") + QString::number(fileId)); return query1.exec(QStringLiteral("DELETE FROM files WHERE id=") + QString::number(fileId)); } static int doRemoveMissingFiles(QSqlDatabase& db, const QString& dbName, QObject *job) { int deletedFiles = 0; QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT files.path FROM files")))) return false; if (!query1.next()) return false; do { QString filePath = query1.value(0).toString(); if (Project::instance()->isFileMissing(filePath)) { qCWarning(LOKALIZE_LOG) << "Removing file " << filePath << " from translation memory"; RemoveFileJob* job_removefile = new RemoveFileJob(filePath, dbName, job); TM::threadPool()->start(job_removefile, REMOVEFILE); deletedFiles++; } } while (query1.next()); return deletedFiles; } static QString escape(QString str) { return str.replace(QLatin1Char('\''), QStringLiteral("''")); } static bool doInsertEntry(CatalogString source, CatalogString target, const QString& ctxt, //TODO QStringList -- after XLIFF bool approved, qlonglong fileId, QSqlDatabase& db, QRegExp& rxClean1,//cleaning regexps for word index update const QString& accel, qlonglong priorId, qlonglong& mainId ) { QTime a; a.start(); mainId = -1; if (Q_UNLIKELY(source.isEmpty())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: source empty"; return false; } bool qpsql = (db.driverName() == QLatin1String("QPSQL")); //we store non-entranslaed entries to make search over all source parts possible bool untranslated = target.isEmpty(); bool shouldBeInIndex = !untranslated && approved; //remove first occurrence of accel character so that search returns words containing accel mark int sourceAccelPos = source.string.indexOf(accel); if (sourceAccelPos != -1) source.string.remove(sourceAccelPos, accel.size()); int targetAccelPos = target.string.indexOf(accel); if (targetAccelPos != -1) target.string.remove(targetAccelPos, accel.size()); //check if we already have record with the same en string QSqlQuery query1(db); QString escapedCtxt = escape(ctxt); QByteArray sourceTags = source.tagsAsByteArray(); QByteArray targetTags = target.tagsAsByteArray(); //BEGIN get sourceId query1.prepare(QString(U("SELECT id FROM source_strings WHERE " "source=? AND (source_accel%1) AND source_markup%2")).arg (sourceAccelPos != -1 ? QStringLiteral("=?") : QStringLiteral("=-1 OR source_accel ISNULL"), sourceTags.isEmpty() ? QStringLiteral(" ISNULL") : QStringLiteral("=?"))); int paranum = 0; query1.bindValue(paranum++, source.string); if (sourceAccelPos != -1) query1.bindValue(paranum++, sourceAccelPos); if (!sourceTags.isEmpty()) query1.bindValue(paranum++, sourceTags); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } qlonglong sourceId; if (!query1.next()) { //BEGIN insert source anew //qCDebug(LOKALIZE_LOG) <<"insert source anew";; ++newTMSourceEntryCount; QString sql = QStringLiteral("INSERT INTO source_strings (source, source_markup, source_accel) VALUES (?, ?, ?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.clear(); query1.prepare(sql); query1.bindValue(0, source.string); query1.bindValue(1, sourceTags); query1.bindValue(2, sourceAccelPos != -1 ? QVariant(sourceAccelPos) : QVariant()); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } sourceId = qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); query1.clear(); //update index if (shouldBeInIndex) addToIndex(sourceId, source.string, rxClean1, accel, db); //END insert source anew } else { sourceId = query1.value(0).toLongLong(); ++reusedTMSourceEntryCount; //qCDebug(LOKALIZE_LOG)<<"SOURCE ALREADY PRESENT"< there will be new record insertion and main table update below } //qCDebug(LOKALIZE_LOG)< tmConfigCache; static void setConfig(QSqlDatabase& db, const TMConfig& c) { QSqlQuery query(db); query.prepare(QStringLiteral("INSERT INTO tm_config (key, value) VALUES (?, ?)")); query.addBindValue(0); query.addBindValue(c.markup); //qCDebug(LOKALIZE_LOG)<<"setting tm db config:"<setPriority(QThread::IdlePriority); if (m_type == TM::Local) { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); QString dbFolder = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QFileInfo fileInfo(dbFolder); if (!fileInfo.exists(dbFolder)) fileInfo.absoluteDir().mkpath(fileInfo.fileName()); db.setDatabaseName(dbFolder % QLatin1Char('/') % m_dbName % TM_DATABASE_EXTENSION); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { qCDebug(LOKALIZE_LOG) << "failed to open db" << db.databaseName() << db.lastError().text(); QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } if (!initSqliteDb(db)) { //need to recreate db ;( QString filename = db.databaseName(); db.close(); QSqlDatabase::removeDatabase(m_dbName); qCWarning(LOKALIZE_LOG) << "We need to recreate the database " << filename; QFile::remove(filename); db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); db.setDatabaseName(filename); m_connectionSuccessful = db.open() && initSqliteDb(db); if (!m_connectionSuccessful) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } } } else { if (QSqlDatabase::contains(m_dbName)) { //reconnect is true QSqlDatabase::database(m_dbName).close(); QSqlDatabase::removeDatabase(m_dbName); } if (!m_connParams.isFilled()) { QFile rdb(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + m_dbName % REMOTETM_DATABASE_EXTENSION); if (!rdb.open(QIODevice::ReadOnly | QIODevice::Text)) { emit done(this); return; } QTextStream rdbParams(&rdb); m_connParams.driver = rdbParams.readLine(); m_connParams.host = rdbParams.readLine(); m_connParams.db = rdbParams.readLine(); m_connParams.user = rdbParams.readLine(); m_connParams.passwd = rdbParams.readLine(); } QSqlDatabase db = QSqlDatabase::addDatabase(m_connParams.driver, m_dbName); db.setHostName(m_connParams.host); db.setDatabaseName(m_connParams.db); db.setUserName(m_connParams.user); db.setPassword(m_connParams.passwd); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } m_connParams.user = db.userName(); initPgDb(db); } } QSqlDatabase db = QSqlDatabase::database(m_dbName); //if (!m_markup.isEmpty()||!m_accel.isEmpty()) if (m_setParams) setConfig(db, m_tmConfig); else m_tmConfig = getConfig(db); qCDebug(LOKALIZE_LOG) << "db" << m_dbName << "opened" << a.elapsed() << m_tmConfig.targetLangCode; getStats(db, m_stat.pairsCount, m_stat.uniqueSourcesCount, m_stat.uniqueTranslationsCount); if (m_type == TM::Local) { db.close(); db.open(); } emit done(this); } CloseDBJob::CloseDBJob(const QString& name) : QObject(), QRunnable() , m_dbName(name) { setAutoDelete(false); } CloseDBJob::~CloseDBJob() { qCDebug(LOKALIZE_LOG) << "closedb dtor" << m_dbName; } void CloseDBJob::run() { if (m_dbName.length()) QSqlDatabase::removeDatabase(m_dbName); emit done(this); } static QString makeAcceledString(QString source, const QString& accel, const QVariant& accelPos) { if (accelPos.isNull()) return source; int accelPosInt = accelPos.toInt(); if (accelPosInt != -1) source.insert(accelPosInt, accel); return source; } SelectJob* TM::initSelectJob(Catalog* catalog, DocPosition pos, QString db, int opt) { SelectJob* job = new SelectJob(catalog->sourceWithTags(pos), catalog->context(pos.entry).first(), catalog->url(), pos, db.isEmpty() ? Project::instance()->projectID() : db); if (opt & Enqueue) { //deletion should be done by receiver, e.g. slotSuggestionsCame() threadPool()->start(job, SELECT); } return job; } SelectJob::SelectJob(const CatalogString& source, const QString& ctxt, const QString& file, const DocPosition& pos, const QString& dbName) : QObject(), QRunnable() , m_source(source) , m_ctxt(ctxt) , m_file(file) , m_dequeued(false) , m_pos(pos) , m_dbName(dbName) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"selectjob"< invertMap(const QMap& source) { //uses the fact that map has its keys always sorted QMap sortingMap; for (QMap::const_iterator i = source.constBegin(); i != source.constEnd(); ++i) { sortingMap.insertMulti(i.value(), i.key()); } return sortingMap; } //returns true if seen translation with >85% bool SelectJob::doSelect(QSqlDatabase& db, QStringList& words, //QList& entries, bool isShort) { bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QMap occurencies; QVector idsForWord; QSqlQuery queryWords(db); //TODO ??? not sure. make another loop before to create QList< QList > then reorder it by size static const QString queryC[] = {U("SELECT ids_long FROM words WHERE word='%1'"), U("SELECT ids_short FROM words WHERE word='%1'") }; QString queryString = queryC[isShort]; //for each word... int o = words.size(); while (--o >= 0) { //if this is not the first word occurrence, just readd ids for it if (!(!idsForWord.isEmpty() && words.at(o) == words.at(o + 1))) { idsForWord.clear(); queryWords.exec(queryString.arg(words.at(o))); if (Q_UNLIKELY(!queryWords.exec(queryString.arg(words.at(o))))) qCWarning(LOKALIZE_LOG) << "select error: " << queryWords.lastError().text() << endl; if (queryWords.next()) { QByteArray arr(queryWords.value(0).toByteArray()); queryWords.clear(); QList ids(arr.split(' ')); int p = ids.size(); idsForWord.reserve(p); while (--p >= 0) idsForWord.append(ids.at(p).toLongLong(/*bool ok*/0, 36)); } else { queryWords.clear(); continue; } } //qCWarning(LOKALIZE_LOG) <<"SelectJob: idsForWord.size() "<::const_iterator i = idsForWord.constBegin(); i != idsForWord.constEnd(); i++) occurencies[*i]++; //0 is default value } //accels are removed TMConfig c = getConfig(db); QString tmp = c.markup; if (!c.markup.isEmpty()) tmp += '|'; QRegExp rxSplit(QLatin1Char('(') % tmp % QStringLiteral("\\W+|\\d+)+")); QString sourceClean(m_source.string); sourceClean.remove(c.accel); //split m_english for use in wordDiff later--all words are needed so we cant use list we already have QStringList englishList(sourceClean.toLower().split(rxSplit, QString::SkipEmptyParts)); static QRegExp delPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); static QRegExp addPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); delPart.setMinimal(true); addPart.setMinimal(true); //QList concordanceLevels=sortedUniqueValues(occurencies); //we start from entries with higher word-concordance level QMap concordanceLevelToIds = invertMap(occurencies); if (concordanceLevelToIds.isEmpty()) return false; bool seen85 = false; int limit = 200; auto clit = concordanceLevelToIds.constEnd(); if (concordanceLevelToIds.size()) --clit; if (concordanceLevelToIds.size()) while (--limit >= 0) { if (Q_UNLIKELY(m_dequeued)) break; //for every concordance level qlonglong level = clit.key(); QString joined; while (level == clit.key()) { joined += QString::number(clit.value()) + ','; if (clit == concordanceLevelToIds.constBegin() || --limit < 0) break; --clit; } joined.chop(1); //get records containing current word QSqlQuery queryFetch(U( "SELECT id, source, source_accel, source_markup FROM source_strings WHERE " "source_strings.id IN (") % joined % ')', db); TMEntry e; while (queryFetch.next()) { e.id = queryFetch.value(0).toLongLong(); if (queryFetch.value(3).toByteArray().size()) qCDebug(LOKALIZE_LOG) << "BA" << queryFetch.value(3).toByteArray(); e.source = CatalogString(makeAcceledString(queryFetch.value(1).toString(), c.accel, queryFetch.value(2)), queryFetch.value(3).toByteArray()); if (e.source.string.contains(TAGRANGE_IMAGE_SYMBOL)) { if (!e.source.tags.size()) qCWarning(LOKALIZE_LOG) << "problem:" << queryFetch.value(3).toByteArray().size() << queryFetch.value(3).toByteArray(); } //e.target=queryFetch.value(2).toString(); //QStringList e_ctxt=queryFetch.value(3).toString().split('\b',QString::SkipEmptyParts); //e.date=queryFetch.value(4).toString(); e.markupExpr = c.markup; e.accelExpr = c.accel; e.dbName = db.connectionName(); //BEGIN calc score QString str = e.source.string; str.remove(c.accel); QStringList englishSuggList(str.toLower().split(rxSplit, QString::SkipEmptyParts)); if (englishSuggList.size() > 10 * englishList.size()) continue; //sugg is 'old' --translator has to adapt its translation to 'new'--current QString result = wordDiff(englishSuggList, englishList); //qCWarning(LOKALIZE_LOG) <<"SelectJob: doin "< 1 so we have decreased it, and increased result: / exp(0.014 * float(addLen) * log10(3.0f + addSubStrCount)); if (delLen) { //qCWarning(LOKALIZE_LOG) <<"SelectJob: delLen:"< 8500; if (seen85 && e.score < 6000) continue; if (e.score < Settings::suggScore() * 100) continue; //BEGIN fetch rest of the data QString change_author_str; QString authors_table_str; if (qpsql) { //change_author_str=", main.change_author "; change_author_str = QStringLiteral(", pg_user.usename "); authors_table_str = QStringLiteral(" JOIN pg_user ON (pg_user.usesysid=main.change_author) "); } QSqlQuery queryRest(U( "SELECT main.id, main.date, main.ctxt, main.bits, " "target_strings.target, target_strings.target_accel, target_strings.target_markup, " "files.path, main.change_date ") % change_author_str % U( "FROM main JOIN target_strings ON (target_strings.id=main.target) JOIN files ON (files.id=main.file) ") % authors_table_str % U("WHERE " "main.source=") % QString::number(e.id) % U(" AND " "(main.bits&4)!=4 AND " "target_strings.target NOTNULL") , db); //ORDER BY tm_main.id ? queryRest.exec(); //qCDebug(LOKALIZE_LOG)<<"main select error"< sortedEntryList; //to eliminate same targets from different files while (queryRest.next()) { e.id = queryRest.value(0).toLongLong(); e.date = queryRest.value(1).toDate(); e.ctxt = queryRest.value(2).toString(); e.target = CatalogString(makeAcceledString(queryRest.value(4).toString(), c.accel, queryRest.value(5)), queryRest.value(6).toByteArray()); QStringList matchData = queryRest.value(2).toString().split(TM_DELIMITER, QString::KeepEmptyParts); //context|plural e.file = queryRest.value(7).toString(); if (e.target.isEmpty()) continue; e.obsolete = queryRest.value(3).toInt() & 1; e.changeDate = queryRest.value(8).toDate(); if (qpsql) e.changeAuthor = queryRest.value(9).toString(); //BEGIN exact match score++ if (possibleExactMatch) { //"exact" match (case insensitive+w/o non-word characters!) if (m_source.string == e.source.string) e.score = 10000; else e.score = 9900; } if (!m_ctxt.isEmpty() && matchData.size() > 0) { //check not needed? if (matchData.at(0) == m_ctxt) e.score += 33; } //qCWarning(LOKALIZE_LOG)<<"m_pos"< 1) { int form = matchData.at(1).toInt(); //pluralMatches=(form&&form==m_pos.form); if (form && form == (int)m_pos.form) { //qCWarning(LOKALIZE_LOG)<<"this"< hash; int oldCount = m_entries.size(); QMap::const_iterator it = sortedEntryList.constEnd(); if (sortedEntryList.size()) while (true) { --it; const TMEntry& e = it.key(); int& hits = hash[e.target.string]; if (!hits) //0 was default value m_entries.append(e); hits++; if (it == sortedEntryList.constBegin()) break; } for (int i = oldCount; i < m_entries.size(); ++i) m_entries[i].hits = hash.value(m_entries.at(i).target.string); //END fetch rest of the data } queryFetch.clear(); if (clit == concordanceLevelToIds.constBegin()) break; if (seen85) limit = qMin(limit, 100); //be more restrictive for the next concordance levels } return seen85; } void SelectJob::run() { //qCDebug(LOKALIZE_LOG)<<"select started"<setPriority(QThread::IdlePriority); QTime a; a.start(); if (Q_UNLIKELY(!QSqlDatabase::contains(m_dbName))) { emit done(this); return; } QSqlDatabase db = QSqlDatabase::database(m_dbName); if (Q_UNLIKELY(!db.isValid() || !db.isOpen())) { emit done(this); return; } //qCDebug(LOKALIZE_LOG)<<"select started 2"<()); const int limit = qMin(Settings::suggCount(), m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = m_entries.size() - 1; while (i >= 0 && (i >= limit || m_entries.last().score < minScore)) { m_entries.removeLast(); i--; } if (Q_UNLIKELY(m_dequeued)) { emit done(this); return; } ++i; while (--i >= 0) { m_entries[i].accelExpr = c.accel; m_entries[i].markupExpr = c.markup; m_entries[i].diff = userVisibleWordDiff(m_entries.at(i).source.string, m_source.string, m_entries.at(i).accelExpr, m_entries.at(i).markupExpr); } emit done(this); } ScanJob::ScanJob(const QString& filePath, const QString& dbName) : QRunnable() , m_filePath(filePath) , m_time(0) , m_added(0) , m_newVersions(0) , m_size(0) , m_dbName(dbName) { qCDebug(LOKALIZE_LOG) << m_dbName << m_filePath; } ScanJob::~ScanJob() { } void ScanJob::run() { if (stop || !QSqlDatabase::contains(m_dbName)) { return; } qCDebug(LOKALIZE_LOG) << "scan job started for" << m_filePath << m_dbName << stop << m_dbName; //QThread::currentThread()->setPriority(QThread::IdlePriority); QTime a; a.start(); QSqlDatabase db = QSqlDatabase::database(m_dbName); if (!db.isOpen()) return; //initSqliteDb(db); TMConfig c = getConfig(db, true); QRegExp rxClean1(c.markup); rxClean1.setMinimal(true); Catalog catalog(0); if (Q_LIKELY(catalog.loadFromUrl(m_filePath, QString(), &m_size, /*no auto save*/true) == 0)) { if (c.targetLangCode != catalog.targetLangCode()) { qCWarning(LOKALIZE_LOG) << "not indexing file because target languages don't match:" << c.targetLangCode << "in TM vs" << catalog.targetLangCode() << "in file"; return; } qlonglong priorId = -1; QSqlQuery queryBegin(QStringLiteral("BEGIN"), db); //qCWarning(LOKALIZE_LOG) <<"queryBegin error: " < #include /** @author Nick Shaforostoff */ class TmxParser : public QXmlDefaultHandler { enum State { //localstate for getting chars into right place null = 0, seg, propContext, propFile, propPluralForm, propApproved }; enum Lang { Source, Target, Null }; public: TmxParser(const QString& dbName); ~TmxParser(); private: bool startDocument() override; bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&) override; bool endElement(const QString&, const QString&, const QString&) override; bool characters(const QString&) override; private: QSqlDatabase db; QRegExp rxClean1; QString accel; int m_hits; CatalogString m_segment[3]; //Lang enum QList m_inlineTags; QString m_context; QString m_pluralForm; QString m_filePath; QString m_approvedString; State m_state: 8; Lang m_lang: 8; ushort m_added; QMap m_fileIds; QString m_dbLangCode; }; TmxParser::TmxParser(const QString& dbName) : m_hits(0) , m_state(null) , m_lang(Null) , m_added(0) , m_dbLangCode(Project::instance()->langCode().toLower()) { db = QSqlDatabase::database(dbName); TMConfig c = getConfig(db); rxClean1.setPattern(c.markup); rxClean1.setMinimal(true); accel = c.accel; } bool TmxParser::startDocument() { //initSqliteDb(db); m_fileIds.clear(); QSqlQuery queryBegin(QLatin1String("BEGIN"), db); m_state = null; m_lang = Null; return true; } TmxParser::~TmxParser() { QSqlQuery queryEnd(QLatin1String("END"), db); } bool TmxParser::startElement(const QString&, const QString&, const QString& qName, const QXmlAttributes& attr) { if (qName == QLatin1String("tu")) { bool ok; m_hits = attr.value(QLatin1String("usagecount")).toInt(&ok); if (!ok) m_hits = -1; m_segment[Source].clear(); m_segment[Target].clear(); m_context.clear(); m_pluralForm.clear(); m_filePath.clear(); m_approvedString.clear(); } else if (qName == QLatin1String("tuv")) { QString attrLang = attr.value(QStringLiteral("xml:lang")).toLower(); if (attrLang == QLatin1String("en")) //TODO startsWith? m_lang = Source; else if (attrLang == m_dbLangCode) m_lang = Target; else { qCWarning(LOKALIZE_LOG) << "skipping lang" << attr.value("xml:lang"); m_lang = Null; } } else if (qName == QLatin1String("prop")) { QString attrType = attr.value(QStringLiteral("type")).toLower(); if (attrType == QLatin1String("x-context")) m_state = propContext; else if (attrType == QLatin1String("x-file")) m_state = propFile; else if (attrType == QLatin1String("x-pluralform")) m_state = propPluralForm; else if (attrType == QLatin1String("x-approved")) m_state = propApproved; else m_state = null; } else if (qName == QLatin1String("seg")) { m_state = seg; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); int pos = m_segment[m_lang].string.size(); m_inlineTags.append(InlineTag(pos, pos, t, attr.value(QStringLiteral("id")))); } } return true; } bool TmxParser::endElement(const QString&, const QString&, const QString& qName) { if (qName == QLatin1String("tu")) { if (m_filePath.isEmpty()) m_filePath = QLatin1String("tmx-import"); if (!m_fileIds.contains(m_filePath)) m_fileIds.insert(m_filePath, getFileId(m_filePath, db)); qlonglong fileId = m_fileIds.value(m_filePath); if (!m_pluralForm.isEmpty()) m_context += TM_DELIMITER + m_pluralForm; qlonglong priorId = -1; bool ok = doInsertEntry(m_segment[Source], m_segment[Target], m_context, m_approvedString != QLatin1String("no"), fileId, db, rxClean1, accel, priorId, priorId); if (Q_LIKELY(ok)) ++m_added; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { InlineTag tag = m_inlineTags.takeLast(); qCWarning(LOKALIZE_LOG) << qName << tag.getElementName(); if (tag.isPaired()) { tag.end = m_segment[m_lang].string.size(); m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); } m_segment[m_lang].tags.append(tag); } } m_state = null; return true; } bool TmxParser::characters(const QString& ch) { if (m_state == seg && m_lang != Null) m_segment[m_lang].string += ch; else if (m_state == propFile) m_filePath += ch; else if (m_state == propContext) m_context += ch; else if (m_state == propPluralForm) m_pluralForm += ch; else if (m_state == propApproved) m_approvedString += ch; return true; } ImportTmxJob::ImportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ImportTmxJob::~ImportTmxJob() { qCDebug(LOKALIZE_LOG) << "ImportTmxJob dtor"; } void ImportTmxJob::run() { QTime a; a.start(); QFile file(m_filename); if (!file.open(QFile::ReadOnly | QFile::Text)) return; TmxParser parser(m_dbName); QXmlSimpleReader reader; reader.setContentHandler(&parser); QXmlInputSource xmlInputSource(&file); if (!reader.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load" << m_filename; //qCWarning(LOKALIZE_LOG) <<"Done scanning "< ExportTmxJob::ExportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ExportTmxJob::~ExportTmxJob() { qCDebug(LOKALIZE_LOG) << "ExportTmxJob dtor"; } void ExportTmxJob::run() { QTime a; a.start(); QFile out(m_filename); if (!out.open(QFile::WriteOnly | QFile::Text)) return; QXmlStreamWriter xmlOut(&out); xmlOut.setAutoFormatting(true); xmlOut.writeStartDocument(QStringLiteral("1.0")); xmlOut.writeStartElement(QStringLiteral("tmx")); xmlOut.writeAttribute(QStringLiteral("version"), QStringLiteral("2.0")); xmlOut.writeStartElement(QStringLiteral("header")); xmlOut.writeAttribute(QStringLiteral("creationtool"), QStringLiteral("lokalize")); xmlOut.writeAttribute(QStringLiteral("creationtoolversion"), QStringLiteral(LOKALIZE_VERSION)); xmlOut.writeAttribute(QStringLiteral("segtype"), QStringLiteral("paragraph")); xmlOut.writeAttribute(QStringLiteral("o-encoding"), QStringLiteral("UTF-8")); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("body")); QString dbLangCode = Project::instance()->langCode(); QSqlDatabase db = QSqlDatabase::database(m_dbName); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U( "SELECT main.id, main.ctxt, main.date, main.bits, " "source_strings.source, source_strings.source_accel, " "target_strings.target, target_strings.target_accel, " "files.path, main.change_date " "FROM main, source_strings, target_strings, files " "WHERE source_strings.id=main.source AND " "target_strings.id=main.target AND " "files.id=main.file")))) qCWarning(LOKALIZE_LOG) << "select error: " << query1.lastError().text(); TMConfig c = getConfig(db); const QString DATE_FORMAT = QStringLiteral("yyyyMMdd"); const QString PROP = QStringLiteral("prop"); const QString TYPE = QStringLiteral("type"); while (query1.next()) { QString source = makeAcceledString(query1.value(4).toString(), c.accel, query1.value(5)); QString target = makeAcceledString(query1.value(6).toString(), c.accel, query1.value(7)); xmlOut.writeStartElement(QStringLiteral("tu")); xmlOut.writeAttribute(QStringLiteral("tuid"), QString::number(query1.value(0).toLongLong())); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), QStringLiteral("en")); xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(source); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), dbLangCode); xmlOut.writeAttribute(QStringLiteral("creationdate"), QDate::fromString(query1.value(2).toString(), Qt::ISODate).toString(DATE_FORMAT)); xmlOut.writeAttribute(QStringLiteral("changedate"), QDate::fromString(query1.value(9).toString(), Qt::ISODate).toString(DATE_FORMAT)); QString ctxt = query1.value(1).toString(); if (!ctxt.isEmpty()) { int pos = ctxt.indexOf(TM_DELIMITER); if (pos != -1) { QString plural = ctxt; plural.remove(0, pos + 1); ctxt.remove(pos, plural.size()); xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-pluralform"); xmlOut.writeCharacters(plural); xmlOut.writeEndElement(); } if (!ctxt.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-context"); xmlOut.writeCharacters(ctxt); xmlOut.writeEndElement(); } } QString filePath = query1.value(8).toString(); if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-file"); xmlOut.writeCharacters(filePath); xmlOut.writeEndElement(); } qlonglong bits = query1.value(8).toLongLong(); if (bits & TM_NOTAPPROVED) if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-approved"); xmlOut.writeCharacters("no"); xmlOut.writeEndElement(); } xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(target); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeEndElement(); } query1.clear(); xmlOut.writeEndDocument(); out.close(); qCWarning(LOKALIZE_LOG) << "ExportTmxJob done exporting:" << a.elapsed(); m_time = a.elapsed(); } //END TMX ExecQueryJob::ExecQueryJob(const QString& queryString, const QString& dbName, QMutex *dbOperation) : QObject(), QRunnable() , query(0) , m_dbName(dbName) , m_query(queryString) , m_dbOperationMutex(dbOperation) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"ExecQueryJob"<lock(); delete query; m_dbOperationMutex->unlock(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob dtor"; } void ExecQueryJob::run() { m_dbOperationMutex->lock(); QSqlDatabase db = QSqlDatabase::database(m_dbName); qCDebug(LOKALIZE_LOG) << "ExecQueryJob" << m_dbName << "db.isOpen() =" << db.isOpen(); //temporarily: if (!db.isOpen()) qCWarning(LOKALIZE_LOG) << "ExecQueryJob db.open()=" << db.open(); query = new QSqlQuery(m_query, db); query->exec(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob done" << query->lastError().text(); m_dbOperationMutex->unlock(); emit done(this); } diff --git a/src/tm/jobs.h b/src/tm/jobs.h index e878fe1..284c1a6 100644 --- a/src/tm/jobs.h +++ b/src/tm/jobs.h @@ -1,495 +1,496 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef JOBS_H #define JOBS_H #include "pos.h" #include "tmentry.h" #include #include #include #include #include class QSqlQuery; /** * Translation Memory classes. see initDb() function for the database scheme */ namespace TM { #define TM_DATABASE_EXTENSION ".db" #define REMOTETM_DATABASE_EXTENSION ".remotedb" enum DbType {Local, Remote, Undefined}; //is needed only on opening #define TM_AREA 8111 QThreadPool* threadPool(); #define CLOSEDB 10001 #define OPENDB 10000 #define TMTABSELECT 100 #define UPDATE 80 #define REMOVE 70 #define REMOVEFILE 69 #define INSERT 60 #define SELECT 50 #define BATCHSELECTFINISHED 49 #define IMPORT 30 #define EXPORT 25 #define REMOVEMISSINGFILES 11 #define SCAN 10 #define SCANFINISHED 9 struct TMConfig { QString markup; QString accel; QString sourceLangCode; QString targetLangCode; }; void cancelAllJobs(); //HACK because threadweaver's dequeue is not workin' //called on startup class OpenDBJob: public QObject, public QRunnable { Q_OBJECT public: struct ConnectionParams { QString driver, host, db, user, passwd; bool isFilled() { return !host.isEmpty() && !db.isEmpty() && !user.isEmpty(); } }; explicit OpenDBJob(const QString& dbName, DbType type = TM::Local, bool reconnect = false, const ConnectionParams& connParams = ConnectionParams()); ~OpenDBJob(); int priority() const { return OPENDB; } struct DBStat { int pairsCount, uniqueSourcesCount, uniqueTranslationsCount; DBStat(): pairsCount(0), uniqueSourcesCount(0), uniqueTranslationsCount(0) {} }; protected: void run() override; signals: void done(OpenDBJob*); public: QString m_dbName; DbType m_type; //statistics DBStat m_stat; //for the new DB creation TMConfig m_tmConfig; bool m_setParams; bool m_connectionSuccessful; bool m_reconnect; ConnectionParams m_connParams; }; //called on startup class CloseDBJob: public QObject, public QRunnable { Q_OBJECT public: explicit CloseDBJob(const QString& dbName); ~CloseDBJob(); int priority() const { return CLOSEDB; } QString dbName() { return m_dbName; } signals: void done(CloseDBJob*); protected: void run() override; QString m_dbName; //statistics? }; class SelectJob: public QObject, public QRunnable { Q_OBJECT public: SelectJob(const CatalogString& source, const QString& ctxt, const QString& file, const DocPosition&,//for back tracking const QString& dbName); ~SelectJob(); int priority() const { return SELECT; } signals: void done(SelectJob*); protected: void run() override; //void aboutToBeDequeued(ThreadWeaver::WeaverInterface*); KDE5PORT private: //returns true if seen translation with >85% bool doSelect(QSqlDatabase&, QStringList& words, bool isShort); public: CatalogString m_source; private: QString m_ctxt; QString m_file; bool m_dequeued; public: DocPosition m_pos; QList m_entries; QString m_dbName; }; enum {Enqueue = 1}; SelectJob* initSelectJob(Catalog*, DocPosition pos, QString db = QString(), int opt = Enqueue); class RemoveMissingFilesJob: public QObject, public QRunnable { Q_OBJECT public: explicit RemoveMissingFilesJob(const QString& dbName); ~RemoveMissingFilesJob(); int priority() const { return REMOVEMISSINGFILES; } protected: void run() override; QString m_dbName; signals: void done(); }; class RemoveFileJob: public QObject, public QRunnable { Q_OBJECT public: explicit RemoveFileJob(const QString& filePath, const QString& dbName, QObject *parent = nullptr); ~RemoveFileJob(); int priority() const { return REMOVEFILE; } protected: void run() override; QString m_filePath; QString m_dbName; QObject m_parent; signals: void done(); }; class RemoveJob: public QObject, public QRunnable { Q_OBJECT public: explicit RemoveJob(const TMEntry& entry); ~RemoveJob(); int priority() const { return REMOVE; } protected: void run() override; TMEntry m_entry; signals: void done(); }; /** * used to eliminate a lot of duplicate entries * * it is supposed to run on entry switch/file close in Editor **/ //TODO a mechanism to get rid of dead dups (use strigi?). //also, display usage of different translations and suggest user //to use only one of them (listview, checkboxes) class UpdateJob: public QRunnable { public: explicit UpdateJob(const QString& filePath, const QString& ctxt, const CatalogString& en, const CatalogString& newTarget, int form, bool approved, //const DocPosition&,//for back tracking const QString& dbName); ~UpdateJob() {} int priority() const { return UPDATE; } protected: void run() override; private: QString m_filePath; QString m_ctxt; CatalogString m_english; CatalogString m_newTarget; int m_form; bool m_approved; QString m_dbName; }; //scan one file class ScanJob: public QRunnable { public: explicit ScanJob(const QString& filePath, const QString& dbName); ~ScanJob(); int priority() const { return SCAN; } protected: void run() override; public: QString m_filePath; //statistics ushort m_time; ushort m_added; ushort m_newVersions;//e1.english==e2.english, e1.target!=e2.target int m_size; QString m_dbName; }; class ScanJobFeedingBack: public QObject, public ScanJob { Q_OBJECT public: explicit ScanJobFeedingBack(const QString& filePath, const QString& dbName) : QObject(), ScanJob(filePath, dbName) { setAutoDelete(false); } protected: void run() override { ScanJob::run(); emit done(this); } signals: void done(ScanJobFeedingBack*); }; //helper class BatchSelectFinishedJob: public QObject, public QRunnable { Q_OBJECT public: explicit BatchSelectFinishedJob(QWidget* view) : QObject(), QRunnable() , m_view(view) {} ~BatchSelectFinishedJob() override {} int priority() const { return BATCHSELECTFINISHED; } signals: void done(); protected: void run() override { emit done(); } public: QWidget* m_view; }; #if 0 we use index stored in db now... //create index --called on startup class IndexWordsJob: public QRunnable { Q_OBJECT public: IndexWordsJob(QObject* parent = nullptr); ~IndexWordsJob(); int priority() const { return 100; } protected: void run(); public: TMWordHash m_tmWordHash; //statistics? }; #endif class ImportTmxJob: public QRunnable { public: explicit ImportTmxJob(const QString& url, const QString& dbName); ~ImportTmxJob(); int priority() const { return IMPORT; } protected: void run() override; public: QString m_filename; //statistics ushort m_time; QString m_dbName; }; // #if 0 class ExportTmxJob: public QRunnable { public: explicit ExportTmxJob(const QString& url, const QString& dbName); ~ExportTmxJob(); int priority() const { return IMPORT; } protected: void run() override; public: QString m_filename; //statistics ushort m_time; QString m_dbName; }; // #endif class ExecQueryJob: public QObject, public QRunnable { Q_OBJECT public: explicit ExecQueryJob(const QString& queryString, const QString& dbName, QMutex *dbOperation); ~ExecQueryJob(); int priority() const { return TMTABSELECT; } QSqlQuery* query; signals: void done(ExecQueryJob*); protected: void run() override; QString m_dbName; QString m_query; QMutex* m_dbOperationMutex; //statistics? }; } #endif diff --git a/src/tm/qamodel.cpp b/src/tm/qamodel.cpp index 6cf0572..89d4eb8 100644 --- a/src/tm/qamodel.cpp +++ b/src/tm/qamodel.cpp @@ -1,234 +1,235 @@ /* This file is part of Lokalize Copyright (C) 2011-2012 Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "qamodel.h" #include "domroutines.h" #include #include #include #include #include static QString ruleTagNames[] = {QString("source"), QString("falseFriend"), QString("target")}; static QStringList domListToStringList(const QDomNodeList& nodes) { QStringList result; result.reserve(nodes.size()); for (int i = 0; i < nodes.size(); i++) result.append(nodes.at(i).toElement().text()); return result; } static QRegExp domNodeToRegExp(const QDomNode& node) { QRegExp re(node.toElement().text()); re.setMinimal(true); return re; } static QVector domListToRegExpVector(const QDomNodeList& nodes) { QVector result; result.reserve(nodes.size()); for (int i = 0; i < nodes.size(); i++) result.append(domNodeToRegExp(nodes.at(i))); return result; } QaModel* QaModel::_instance = 0; void QaModel::cleanupQaModel() { delete QaModel::_instance; QaModel::_instance = 0; } bool QaModel::isInstantiated() { return _instance != 0; } QaModel* QaModel::instance() { if (Q_UNLIKELY(_instance == 0)) { _instance = new QaModel; qAddPostRoutine(QaModel::cleanupQaModel); } return _instance; } QaModel::QaModel(QObject* parent): QAbstractListModel(parent) { } QaModel::~QaModel() { saveRules(); } int QaModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_entries.count(); } QVariant QaModel::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { //case ID: return i18nc("@title:column","ID"); case Source: return i18nc("@title:column Original text", "Source");; case FalseFriend: return i18nc("@title:column Translator's false friend", "False Friend"); } return QVariant(); } QVariant QaModel::data(const QModelIndex& item, int role) const { if (role == Qt::ToolTipRole) return m_filename; if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); static const QString nl("\n"); const QDomElement& entry = m_entries.at(item.row()).toElement(); return domListToStringList(entry.elementsByTagName(ruleTagNames[item.column()])).join(nl); } QVector QaModel::toVector() const { QVector rules; QDomNodeList m_categories = m_doc.elementsByTagName("category"); for (int i = 0; i < m_categories.size(); i++) { static const QString ruleTagName("rule"); QDomNodeList m_rules = m_categories.at(i).toElement().elementsByTagName(ruleTagName); for (int j = 0; j < m_rules.size(); j++) { Rule rule; rule.sources = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName(ruleTagNames[Source])); rule.falseFriends = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName(ruleTagNames[FalseFriend])); rule.targets = domListToRegExpVector(m_rules.at(j).toElement().elementsByTagName("target")); rules.append(rule); } } return rules; } bool QaModel::loadRules(const QString& filename) { QFile file(filename); if (file.open(QIODevice::ReadOnly)) { bool ok = m_doc.setContent(&file); file.close(); if (!ok) return false; } else { m_doc.setContent(QByteArray( "\n" "\n" " \n" " \n" "\n")); } m_entries = m_doc.elementsByTagName("rule"); m_filename = filename; return true; } bool QaModel::saveRules(QString filename) { if (filename.isEmpty()) filename = m_filename; if (filename.isEmpty()) return false; QFile device(filename); if (!device.open(QFile::WriteOnly | QFile::Truncate)) return false; QTextStream stream(&device); m_doc.save(stream, 2); //setClean(true); return true; } QModelIndex QaModel::appendRow() { beginInsertRows(QModelIndex(), rowCount(), rowCount()); QDomElement category = m_doc.elementsByTagName("qa").at(0).toElement().elementsByTagName("category").at(0).toElement(); QDomElement rule = category.appendChild(m_doc.createElement("rule")).toElement(); rule.appendChild(m_doc.createElement(ruleTagNames[Source])); rule.appendChild(m_doc.createElement(ruleTagNames[FalseFriend])); endInsertRows(); return index(m_entries.count() - 1); } void QaModel::removeRow(const QModelIndex& rowIndex) { //TODO optimize for contiguous selections beginRemoveRows(QModelIndex(), rowIndex.row(), rowIndex.row()); QDomElement category = m_doc.elementsByTagName("qa").at(0).toElement().elementsByTagName("category").at(0).toElement(); category.removeChild(m_entries.at(rowIndex.row())); endRemoveRows(); } Qt::ItemFlags QaModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; } bool QaModel::setData(const QModelIndex& item, const QVariant& value, int role) { if (role != Qt::DisplayRole && role != Qt::EditRole) return false; QDomElement entry = m_entries.at(item.row()).toElement(); QDomNodeList sources = entry.elementsByTagName(ruleTagNames[item.column()]); QStringList newSources = value.toString().split('\n'); while (sources.size() < newSources.size()) entry.insertAfter(m_doc.createElement(ruleTagNames[item.column()]), sources.at(sources.size() - 1)); while (sources.size() > newSources.size()) entry.removeChild(sources.at(sources.size() - 1)); for (int i = 0; i < sources.size(); i++) setText(sources.at(i).toElement(), newSources.at(i)); emit dataChanged(item, item); return true; } diff --git a/src/tm/qamodel.h b/src/tm/qamodel.h index 351c3ef..efbd1c1 100644 --- a/src/tm/qamodel.h +++ b/src/tm/qamodel.h @@ -1,81 +1,82 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011-2012 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 QAMODEL_H #define QAMODEL_H #include "rule.h" #include #include class QaModel: public QAbstractListModel { //Q_OBJECT public: enum Columns { //ID = 0, Source = 0, FalseFriend, ColumnCount }; explicit QaModel(QObject* parent = nullptr/*, Glossary* glossary*/); ~QaModel() override; bool loadRules(const QString& filename); bool saveRules(QString filename = QString()); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override { return ColumnCount; Q_UNUSED(parent) } QVariant headerData(int section, Qt::Orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex&) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const override; QVector toVector() const; QModelIndex appendRow(); void removeRow(const QModelIndex&); static QaModel* instance(); static bool isInstantiated(); private: static QaModel* _instance; static void cleanupQaModel(); private: QDomDocument m_doc; QDomNodeList m_entries; QString m_filename; }; #endif // QAMODEL_H diff --git a/src/tm/qaview.cpp b/src/tm/qaview.cpp index 97135c7..b1619b7 100644 --- a/src/tm/qaview.cpp +++ b/src/tm/qaview.cpp @@ -1,120 +1,121 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "qaview.h" #include "qamodel.h" #include "project.h" #include #include #include #include QaView::QaView(QWidget* parent) : QDockWidget(i18nc("@title:window", "Quality Assurance"), parent) , m_browser(new QTreeView(this)) { setObjectName(QStringLiteral("QaView")); if (!QaModel::isInstantiated()) QaModel::instance()->loadRules(Project::instance()->qaPath()); m_qaModel = QaModel::instance(); setWidget(m_browser); m_browser->setModel(m_qaModel); m_browser->setRootIsDecorated(false); m_browser->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* action = new QAction(i18nc("@action:inmenu", "Add"), m_browser); connect(action, &QAction::triggered, this, &QaView::addRule); m_browser->addAction(action); action = new QAction(i18nc("@action:inmenu", "Remove"), m_browser); connect(action, &QAction::triggered, this, &QaView::removeRule); m_browser->addAction(action); m_browser->setAlternatingRowColors(true); connect(m_qaModel, &QaModel::dataChanged, this, &QaView::rulesChanged); } QaView::~QaView() { } bool QaView::loadRules(QString filename) { if (filename.isEmpty()) filename = Project::instance()->qaPath(); bool ok = m_qaModel->loadRules(filename); if (ok) m_filename = filename; return ok; } bool QaView::saveRules(QString filename) { return m_qaModel->saveRules(filename.isEmpty() ? m_filename : filename); } QVector< Rule > QaView::rules() const { return m_qaModel->toVector(); } void QaView::addRule() { QModelIndex newRule = m_qaModel->appendRow(); m_browser->selectionModel()->select(newRule, QItemSelectionModel::ClearAndSelect); m_browser->edit(newRule); } void QaView::removeRule() { foreach (const QModelIndex& rowIndex, m_browser->selectionModel()->selectedRows()) m_qaModel->removeRow(rowIndex); } int findMatchingRule(const QVector& rules, const QString& source, const QString& target, QVector& positions) { for (QVector::const_iterator it = rules.constBegin(); it != rules.constEnd(); it++) { if (it->sources.first().indexIn(source) != -1) { if (it->falseFriends.first().indexIn(target) != -1) { if (positions.size()) { positions[0].start = it->sources.first().pos(); positions[0].len = it->sources.first().matchedLength(); positions[1].start = it->falseFriends.first().pos(); positions[1].len = it->falseFriends.first().matchedLength(); } return it - rules.constBegin(); } } } return -1; } diff --git a/src/tm/qaview.h b/src/tm/qaview.h index 38d6d1d..44060d1 100644 --- a/src/tm/qaview.h +++ b/src/tm/qaview.h @@ -1,68 +1,69 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef QAVIEW_H #define QAVIEW_H #include #include #include #include "rule.h" class QaModel; class QaView: public QDockWidget { Q_OBJECT public: explicit QaView(QWidget*); ~QaView(); bool loadRules(QString filename = QString()); bool saveRules(QString filename = QString()); QVector rules() const; public slots: void addRule(); void removeRule(); signals: void rulesChanged(); private: QTreeView* m_browser; QaModel* m_qaModel; QString m_filename; QVector m_rules; }; int findMatchingRule(const QVector& rules, const QString& source, const QString& target, QVector& positions); #endif // QAVIEW_H diff --git a/src/tm/rule.h b/src/tm/rule.h index a252ff6..ff318e7 100644 --- a/src/tm/rule.h +++ b/src/tm/rule.h @@ -1,51 +1,52 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef QARULE_H #define QARULE_H #include #include #include struct StringRule { QVector sources; QVector targets; QVector falseFriends; }; struct Rule { QVector sources; QVector targets; QVector falseFriends; }; struct StartLen { short start; short len; StartLen(short s = 0, short l = 0): start(s), len(l) {} }; #endif diff --git a/src/tm/tmentry.h b/src/tm/tmentry.h index 5c4686f..27d3201 100644 --- a/src/tm/tmentry.h +++ b/src/tm/tmentry.h @@ -1,74 +1,75 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 TMENTRY_H #define TMENTRY_H #include "catalogstring.h" #include #include namespace TM { struct TMEntry { CatalogString source; CatalogString target; QString ctxt; QString file; QDate date; QDate changeDate; QString changeAuthor; //the remaining are used only for results qlonglong id; short score: 16; //100.00%==10000 ushort hits: 15; bool obsolete: 1; QString dbName; QString diff; //different databases can have different settings: QString accelExpr; QString markupExpr; bool operator<(const TMEntry& other) const { if (score == other.score) { if (hits == other.hits) return date < other.date; return hits < other.hits; } return score < other.score; } TMEntry(): id(-1), score(0), hits(0), obsolete(false) {} }; } #endif diff --git a/src/tm/tmmanager.cpp b/src/tm/tmmanager.cpp index fda24c4..e78458b 100644 --- a/src/tm/tmmanager.cpp +++ b/src/tm/tmmanager.cpp @@ -1,281 +1,282 @@ /* **************************************************************************** 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) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "tmmanager.h" #include "lokalize_debug.h" #include "ui_managedatabases.h" #include "dbfilesmodel.h" #include "tmtab.h" #include "jobs.h" #include "tmscanapi.h" #include "project.h" #include "languagelistmodel.h" #include #include #include #include #include #include using namespace TM; TMManagerWin::TMManagerWin(QWidget *parent) : KMainWindow(parent) { setAttribute(Qt::WA_DeleteOnClose, false); setCaption(i18nc("@title:window", "Translation Memories")); setCentralWidget(new QWidget(this)); Ui_TMManager ui_tmManager; ui_tmManager.setupUi(centralWidget()); ui_tmManager.list->setModel(DBFilesModel::instance()); ui_tmManager.list->setRootIndex(DBFilesModel::instance()->rootIndex()); m_tmListWidget = ui_tmManager.list; connect(ui_tmManager.addData, &QPushButton::clicked, this, &TMManagerWin::addDir); connect(ui_tmManager.create, &QPushButton::clicked, this, &TMManagerWin::addDB); connect(ui_tmManager.importTMX, &QPushButton::clicked, this, &TMManagerWin::importTMX); connect(ui_tmManager.exportTMX, &QPushButton::clicked, this, &TMManagerWin::exportTMX); connect(ui_tmManager.remove, &QPushButton::clicked, this, &TMManagerWin::removeDB); QTimer::singleShot(100, this, &TMManagerWin::initLater); } void TMManagerWin::initLater() { connect(m_tmListWidget, &QTreeView::activated, this, &TMManagerWin::slotItemActivated); QPersistentModelIndex* projectDBIndex = DBFilesModel::instance()->projectDBIndex(); if (projectDBIndex) m_tmListWidget->setCurrentIndex(*projectDBIndex); } void TMManagerWin::addDir() { QModelIndex index = m_tmListWidget->currentIndex(); if (!index.isValid()) return; QString dir = QFileDialog::getExistingDirectory(this, i18nc("@title:window", "Select Directory to be scanned"), Project::instance()->translationsRoot()); if (!dir.isEmpty()) scanRecursive(QStringList(dir), index.sibling(index.row(), 0).data().toString()); } DBPropertiesDialog::DBPropertiesDialog(QWidget* parent, const QString& dbName) : QDialog(parent), Ui_DBParams() , m_connectionOptionsValid(false) { setAttribute(Qt::WA_DeleteOnClose, true); setWindowTitle(dbName.isEmpty() ? i18nc("@title:window", "New Translation Memory") : i18nc("@title:window", "Translation Memory Properties")); setupUi(this); connect(buttonBox, &QDialogButtonBox::accepted, this, &DBPropertiesDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &DBPropertiesDialog::reject); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(name, &QLineEdit::textChanged, this, &DBPropertiesDialog::feedbackRegardingAcceptable); name->setFocus(); sourceLang->setModel(LanguageListModel::instance()->sortModel()); targetLang->setModel(LanguageListModel::instance()->sortModel()); if (dbName.isEmpty()) { accel->setText(Project::instance()->accel()); markup->setText(Project::instance()->markup()); sourceLang->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(Project::instance()->sourceLangCode())); targetLang->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(Project::instance()->targetLangCode())); } connectionBox->hide(); connect(dbType, QOverload::of(&QComboBox::activated), this, &DBPropertiesDialog::setConnectionBoxVisible); m_checkDelayer.setInterval(2000); m_checkDelayer.setSingleShot(true); connect(&m_checkDelayer, &QTimer::timeout, this, &DBPropertiesDialog::checkConnectionOptions); connect(this->dbName, &QLineEdit::textChanged, &m_checkDelayer, QOverload<>::of(&QTimer::start)); connect(dbHost->lineEdit(), &QLineEdit::textChanged, &m_checkDelayer, QOverload<>::of(&QTimer::start)); connect(dbUser, &QLineEdit::textChanged, &m_checkDelayer, QOverload<>::of(&QTimer::start)); connect(dbPasswd, &QLineEdit::textChanged, &m_checkDelayer, QOverload<>::of(&QTimer::start)); QStringList drivers = QSqlDatabase::drivers(); if (drivers.contains("QPSQL")) dbType->addItem("PostgreSQL"); } void DBPropertiesDialog::setConnectionBoxVisible(int type) { connectionBox->setVisible(type); contentBox->setVisible(!type || m_connectionOptionsValid); } void DBPropertiesDialog::feedbackRegardingAcceptable() { buttonBox->button(QDialogButtonBox::Ok)->setEnabled(contentBox->isVisible() && !name->text().isEmpty()); } void DBPropertiesDialog::checkConnectionOptions() { m_connectionOptionsValid = false; if (!connectionBox->isVisible() || name->text().isEmpty() || dbHost->currentText().isEmpty() || dbName->text().isEmpty() || dbUser->text().isEmpty()) return; OpenDBJob::ConnectionParams connParams; connParams.driver = "QPSQL"; connParams.host = dbHost->currentText(); connParams.db = dbName->text(); connParams.user = dbUser->text(); connParams.passwd = dbPasswd->text(); OpenDBJob* openDBJob = new OpenDBJob(name->text(), TM::Remote, /*reconnect*/true, connParams); connect(openDBJob, &OpenDBJob::done, this, &DBPropertiesDialog::openJobDone); threadPool()->start(openDBJob, OPENDB); } void DBPropertiesDialog::openJobDone(OpenDBJob* openDBJob) { openDBJob->deleteLater(); if (!connectionBox->isVisible()) //smth happened while we were trying to connect return; contentBox->setVisible(openDBJob->m_connectionSuccessful); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(openDBJob->m_connectionSuccessful); if (!openDBJob->m_connectionSuccessful) return; sourceLang->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(openDBJob->m_tmConfig.sourceLangCode)); targetLang->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(openDBJob->m_tmConfig.targetLangCode)); markup->setText(openDBJob->m_tmConfig.markup); accel->setText(openDBJob->m_tmConfig.accel); contentBox->show(); dbHost->lineEdit()->setText(openDBJob->m_connParams.host); dbName->setText(openDBJob->m_connParams.db); dbUser->setText(openDBJob->m_connParams.user); dbPasswd->setText(openDBJob->m_connParams.passwd); } void DBPropertiesDialog::accept() { if (name->text().isEmpty() || !contentBox->isVisible()) return; if (connectionBox->isVisible()) { QFile rdb(QStandardPaths::writableLocation(QStandardPaths::DataLocation) % QLatin1Char('/') % name->text() % REMOTETM_DATABASE_EXTENSION); if (!rdb.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return; QTextStream rdbParams(&rdb); rdbParams << "QPSQL" << "\n"; rdbParams << dbHost->currentText() << "\n"; rdbParams << dbName->text() << "\n"; rdbParams << dbUser->text() << "\n"; rdbParams << dbPasswd->text() << "\n"; } OpenDBJob* openDBJob = new OpenDBJob(name->text(), TM::DbType(connectionBox->isVisible()), true); connect(openDBJob, &OpenDBJob::done, DBFilesModel::instance(), &DBFilesModel::updateProjectTmIndex); openDBJob->m_setParams = true; openDBJob->m_tmConfig.markup = markup->text(); openDBJob->m_tmConfig.accel = accel->text(); openDBJob->m_tmConfig.sourceLangCode = LanguageListModel::instance()->langCodeForSortModelRow(sourceLang->currentIndex()); openDBJob->m_tmConfig.targetLangCode = LanguageListModel::instance()->langCodeForSortModelRow(targetLang->currentIndex()); DBFilesModel::instance()->openDB(openDBJob); QDialog::accept(); } void TMManagerWin::addDB() { DBPropertiesDialog* dialog = new DBPropertiesDialog(this); dialog->show(); } void TMManagerWin::removeDB() { QModelIndex index = m_tmListWidget->currentIndex(); if (index.isValid()) DBFilesModel::instance()->removeTM(index); } void TMManagerWin::importTMX() { QString path = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Select TMX file to be imported into selected database"), QString(), i18n("TMX files (*.tmx *.xml)")); QModelIndex index = m_tmListWidget->currentIndex(); if (!index.isValid()) return; QString dbName = index.sibling(index.row(), 0).data().toString(); if (!path.isEmpty()) { ImportTmxJob* j = new ImportTmxJob(path, dbName); threadPool()->start(j, IMPORT); DBFilesModel::instance()->openDB(dbName); //update stats after it finishes } } void TMManagerWin::exportTMX() { //TODO ask whether to save full paths of files, or just their names QString path = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Select TMX file to export selected database to"), QString(), i18n("TMX files (*.tmx *.xml)")); QModelIndex index = m_tmListWidget->currentIndex(); if (!index.isValid()) return; QString dbName = index.sibling(index.row(), 0).data().toString(); if (!path.isEmpty()) { ExportTmxJob* j = new ExportTmxJob(path, dbName); threadPool()->start(j, EXPORT); } } void TMManagerWin::slotItemActivated(const QModelIndex&) { //QString dbName=DBFilesModel::instance()->data(m_tmListWidget->currentIndex()).toString(); /* TMWindow* win=new TMWindow; win->selectDB(m_tmListWidget->currentIndex().row()); win->show();*/ } diff --git a/src/tm/tmmanager.h b/src/tm/tmmanager.h index 0728605..eb547fb 100644 --- a/src/tm/tmmanager.h +++ b/src/tm/tmmanager.h @@ -1,93 +1,94 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef TMMANAGER_H #define TMMANAGER_H #include #include #include #include class QTreeView; #include "ui_dbparams.h" namespace TM { /** * Window for managing Translation Memory databases */ class TMManagerWin: public KMainWindow { Q_OBJECT public: explicit TMManagerWin(QWidget *parent = nullptr); ~TMManagerWin() {} private slots: void addDir(); void addDB(); void importTMX(); void exportTMX(); void removeDB(); void initLater(); void slotItemActivated(const QModelIndex&); private: QTreeView* m_tmListWidget; }; class OpenDBJob; //TODO remote tms class DBPropertiesDialog: public QDialog, Ui_DBParams { Q_OBJECT public: explicit DBPropertiesDialog(QWidget* parent, const QString& name = QString()); private: //void slotButtonClicked(int button); void accept() override; private slots: void setConnectionBoxVisible(int type); void openJobDone(OpenDBJob*); void checkConnectionOptions(); void feedbackRegardingAcceptable(); private: bool m_connectionOptionsValid; QTimer m_checkDelayer; }; } #endif diff --git a/src/tm/tmscanapi.cpp b/src/tm/tmscanapi.cpp index 40a503a..f0e561c 100644 --- a/src/tm/tmscanapi.cpp +++ b/src/tm/tmscanapi.cpp @@ -1,181 +1,182 @@ /* **************************************************************************** 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 "tmscanapi.h" #include "lokalize_debug.h" #include "jobs.h" #include "catalog.h" #include "prefs_lokalize.h" #include "gettextheader.h" #include "dbfilesmodel.h" #include "project.h" #include #include #include #include namespace TM { static QVector doScanRecursive(const QDir& dir, const QString& dbName, KJob* metaJob); } using namespace TM; RecursiveScanJob::RecursiveScanJob(const QString& dbName, QObject* parent) : KJob(parent) , m_dbName(dbName) { setCapabilities(KJob::Killable); } bool RecursiveScanJob::doKill() { #if QT_VERSION >= 0x050500 foreach (ScanJob* job, m_jobs) TM::threadPool()->cancel(job); #endif return true; } void RecursiveScanJob::setJobs(const QVector& jobs) { m_jobs = jobs; setTotalAmount(KJob::Files, jobs.size()); if (!jobs.size()) kill(KJob::EmitResult); } void RecursiveScanJob::scanJobFinished(ScanJobFeedingBack* j) { j->deleteLater(); ScanJob* job = static_cast(j); setProcessedAmount(KJob::Files, processedAmount(KJob::Files) + 1); emitPercent(processedAmount(KJob::Files), totalAmount(KJob::Files)); setProcessedAmount(KJob::Bytes, processedAmount(KJob::Bytes) + job->m_size); if (m_time.elapsed()) emitSpeed(1000 * processedAmount(KJob::Bytes) / m_time.elapsed()); if (processedAmount(KJob::Files) == totalAmount(KJob::Files)) { emitResult(); qCDebug(LOKALIZE_LOG) << "finished in" << m_time.elapsed() << "msecs"; } } void RecursiveScanJob::start() { m_time.start(); emit description(this, i18n("Adding files to Lokalize translation memory"), qMakePair(i18n("TM"), m_dbName)); } int TM::scanRecursive(const QStringList& filePaths, const QString& dbName) { RecursiveScanJob* metaJob = new RecursiveScanJob(dbName); KIO::getJobTracker()->registerJob(metaJob); metaJob->start(); if (!askAuthorInfoIfEmpty()) return 0; QVector result; int i = filePaths.size(); while (--i >= 0) { const QString& filePath = filePaths.at(i); if (filePath.isEmpty()) continue; if (Catalog::extIsSupported(filePath)) { ScanJobFeedingBack* job = new ScanJobFeedingBack(filePath, dbName); QObject::connect(job, &ScanJobFeedingBack::done, metaJob, &RecursiveScanJob::scanJobFinished); TM::threadPool()->start(job, SCAN); result.append(job); } else result += doScanRecursive(QDir(filePath), dbName, metaJob); } metaJob->setJobs(result); DBFilesModel::instance()->openDB(dbName); //update stats after it finishes return result.size(); } //returns gross number of jobs started static QVector TM::doScanRecursive(const QDir& dir, const QString& dbName, KJob* metaJob) { QVector result; QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) result += TM::doScanRecursive(QDir(dir.filePath(subDirs.at(i))), dbName, metaJob); QStringList filters = Catalog::supportedExtensions(); i = filters.size(); while (--i >= 0) filters[i].prepend('*'); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); while (--i >= 0) { ScanJobFeedingBack* job = new ScanJobFeedingBack(dir.filePath(files.at(i)), dbName); QObject::connect(job, &ScanJobFeedingBack::done, (RecursiveScanJob*)metaJob, &RecursiveScanJob::scanJobFinished); TM::threadPool()->start(job, SCAN); result.append(job); } return result; } bool dragIsAcceptable(const QList& urls) { int i = urls.size(); while (--i >= 0) { bool ok = Catalog::extIsSupported(urls.at(i).path()); if (!ok) { QFileInfo info(urls.at(i).toLocalFile()); ok = info.exists() && info.isDir(); } if (ok) return true; } return false; } QString shorterFilePath(const QString path) { if (!Project::instance()->isLoaded()) return path; QString pDir = Project::instance()->projectDir(); if (path.startsWith(pDir))//TODO cache projectDir? return QDir(pDir).relativeFilePath(path); return path; } diff --git a/src/tm/tmscanapi.h b/src/tm/tmscanapi.h index 7480124..72a81e2 100644 --- a/src/tm/tmscanapi.h +++ b/src/tm/tmscanapi.h @@ -1,69 +1,70 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef SCANAPI_H #define SCANAPI_H #include #include #include #include #include bool dragIsAcceptable(const QList& urls); QString shorterFilePath(const QString path); namespace TM { class ScanJob; class ScanJobFeedingBack; void purgeMissingFilesFromTM(const QStringList& urls, const QString& dbName); ///wrapper. returns gross number of jobs started int scanRecursive(const QStringList& urls, const QString& dbName); class RecursiveScanJob: public KJob { Q_OBJECT public: explicit RecursiveScanJob(const QString& dbName, QObject* parent = nullptr); void setJobs(const QVector& jobs); void start() override; public slots: void scanJobFinished(ScanJobFeedingBack*); protected: bool doKill() override; private: QString m_dbName; QTime m_time; QVector m_jobs; }; } #endif diff --git a/src/tm/tmtab.cpp b/src/tm/tmtab.cpp index fc63df4..740983f 100644 --- a/src/tm/tmtab.cpp +++ b/src/tm/tmtab.cpp @@ -1,794 +1,795 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "tmtab.h" #include "lokalize_debug.h" #include "ui_queryoptions.h" #include "project.h" #include "dbfilesmodel.h" #include "tmscanapi.h" #include "qaview.h" #include "prefs_lokalize.h" #include "jobs.h" #include "fastsizehintitemdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) && defined(QStringLiteral) #undef QStringLiteral #define QStringLiteral QLatin1String #endif using namespace TM; //static int BIG_COUNTER=0; //TODO do things for case when user explicitly wants to find & accel mark //BEGIN TMDBModel TMDBModel::TMDBModel(QObject* parent) : QSqlQueryModel(parent) , m_queryType(WordOrder) , m_totalResultCount(0) { setHeaderData(TMDBModel::Source, Qt::Horizontal, i18nc("@title:column Original text", "Source")); setHeaderData(TMDBModel::Target, Qt::Horizontal, i18nc("@title:column Text in target language", "Target")); setHeaderData(TMDBModel::Context, Qt::Horizontal, i18nc("@title:column", "Context")); setHeaderData(TMDBModel::Filepath, Qt::Horizontal, i18nc("@title:column", "File")); setHeaderData(TMDBModel::TransationStatus, Qt::Horizontal, i18nc("@title:column", "Translation Status")); } void TMDBModel::setDB(const QString& str) { m_dbName = str; QString sourceLangCode = DBFilesModel::instance()->m_configurations.value(str).sourceLangCode; QString targetLangCode = DBFilesModel::instance()->m_configurations.value(str).targetLangCode; if (sourceLangCode.length()) setHeaderData(TMDBModel::Source, Qt::Horizontal, QString(i18nc("@title:column Original text", "Source") % QStringLiteral(": ") % sourceLangCode)); if (targetLangCode.length()) setHeaderData(TMDBModel::Target, Qt::Horizontal, QString(i18nc("@title:column Text in target language", "Target") % QStringLiteral(": ") % targetLangCode)); } void TMDBModel::setQueryType(int type) { m_queryType = (QueryType)type; } void TMDBModel::setFilter(const QString& source, const QString& target, bool invertSource, bool invertTarget, const QString& filemask ) { QString escapedSource(source); escapedSource.replace('\'', QStringLiteral("''")); QString escapedTarget(target); escapedTarget.replace('\'', QStringLiteral("''")); QString invertSourceStr; if (invertSource) invertSourceStr = QStringLiteral("NOT "); QString invertTargetStr; if (invertTarget) invertTargetStr = QStringLiteral("NOT "); QString escapedFilemask(filemask); escapedFilemask.replace('\'', QStringLiteral("''")); QString sourceQuery; QString targetQuery; QString fileQuery; if (m_queryType == SubStr) { escapedSource.replace('%', QStringLiteral("\b%")); escapedSource.replace('_', QStringLiteral("\b_")); escapedTarget.replace('%', QStringLiteral("\b%")); escapedTarget.replace('_', QStringLiteral("\b_")); if (!escapedSource.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") % invertSourceStr % QStringLiteral("LIKE '%") % escapedSource % QStringLiteral("%' ESCAPE '\b' "); if (!escapedTarget.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") % invertTargetStr % QStringLiteral("LIKE '%") % escapedTarget % QStringLiteral("%' ESCAPE '\b' "); } else if (m_queryType == WordOrder) { /*escapedSource.replace('%',"\b%");escapedSource.replace('_',"\b_"); escapedTarget.replace('%',"\b%");escapedTarget.replace('_',"\b_");*/ QRegExp wre(QStringLiteral("\\W")); QStringList sourceList = escapedSource.split(wre, QString::SkipEmptyParts); QStringList targetList = escapedTarget.split(wre, QString::SkipEmptyParts); if (!sourceList.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") % invertSourceStr % QStringLiteral("LIKE '%") % sourceList.join(QStringLiteral("%' AND source_strings.source ") % invertSourceStr % QStringLiteral("LIKE '%")) % QStringLiteral("%' "); if (!targetList.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") % invertTargetStr % QStringLiteral("LIKE '%") % targetList.join(QStringLiteral("%' AND target_strings.target ") % invertTargetStr % QStringLiteral("LIKE '%")) % QStringLiteral("%' "); } else { if (!escapedSource.isEmpty()) sourceQuery = QStringLiteral("AND source_strings.source ") % invertSourceStr % QStringLiteral("GLOB '") % escapedSource % QStringLiteral("' "); if (!escapedTarget.isEmpty()) targetQuery = QStringLiteral("AND target_strings.target ") % invertTargetStr % QStringLiteral("GLOB '") % escapedTarget % QStringLiteral("' "); } if (!filemask.isEmpty()) fileQuery = QStringLiteral("AND files.path GLOB '") % escapedFilemask % QStringLiteral("' "); QString fromPart = QStringLiteral("FROM main JOIN source_strings ON (source_strings.id=main.source) " "JOIN target_strings ON (target_strings.id=main.target), files " "WHERE files.id=main.file ") % sourceQuery % targetQuery % fileQuery; ExecQueryJob* job = new ExecQueryJob(QStringLiteral( "SELECT source_strings.source, target_strings.target, " "main.ctxt, files.path, " "source_strings.source_accel, target_strings.target_accel, main.bits ") + fromPart, m_dbName, &m_dbOperationMutex); connect(job, &ExecQueryJob::done, this, &TMDBModel::slotQueryExecuted); threadPool()->start(job); job = new ExecQueryJob(QStringLiteral("SELECT count(*) ") + fromPart, m_dbName, &m_dbOperationMutex); connect(job, &ExecQueryJob::done, this, &TMDBModel::slotQueryExecuted); threadPool()->start(job); m_totalResultCount = 0; } void TMDBModel::slotQueryExecuted(ExecQueryJob* job) { job->deleteLater(); m_dbOperationMutex.lock(); if (job->query->lastQuery().startsWith(QLatin1String("SELECT count(*) "))) { m_totalResultCount = job->query->next() ? job->query->value(0).toInt() : -1; m_dbOperationMutex.unlock(); emit finalResultCountFetched(m_totalResultCount); return; } query().finish(); query().clear(); setQuery(*(job->query)); m_dbOperationMutex.unlock(); emit resultsFetched(); } bool TMDBModel::rowIsApproved(int row) const { bool ok; qlonglong bits = record(row).value(TMDBModel::_Bits).toLongLong(&ok); return !(ok && bits & 4); } int TMDBModel::translationStatus(const QModelIndex& item) const { //QMutexLocker locker(&m_dbOperationMutex); if (QSqlQueryModel::data(item.sibling(item.row(), Target), Qt::DisplayRole).toString().isEmpty()) return 2; return int(!rowIsApproved(item.row())); } #define TM_DELIMITER '\v' QVariant TMDBModel::data(const QModelIndex& item, int role) const { //QMutexLocker locker(&m_dbOperationMutex); bool doHtml = (role == FastSizeHintItemDelegate::HtmlDisplayRole); if (doHtml) role = Qt::DisplayRole; else if (role == Qt::FontRole && item.column() == TMDBModel::Target) { //TODO Qt::ForegroundRole -- brush for orphaned entries QFont font = QApplication::font(); font.setItalic(!rowIsApproved(item.row())); return font; } else if (role == FullPathRole && item.column() == TMDBModel::Filepath) return QSqlQueryModel::data(item, Qt::DisplayRole); else if (role == TransStateRole) return translationStatus(item); QVariant result = QSqlQueryModel::data(item, role); /* if (role==Qt::SizeHintRole && !result.isValid()) BIG_COUNTER++;*/ if (role != Qt::DisplayRole) return result; if (item.column() == TMDBModel::Context) { //context QString r = result.toString(); int pos = r.indexOf(TM_DELIMITER); if (pos != -1) result = r.remove(pos, r.size()); } else if (item.column() < TMDBModel::Context && !record(item.row()).isNull(TMDBModel::_SourceAccel + item.column())) { //source, target const QVariant& posVar = record(item.row()).value(TMDBModel::_SourceAccel + item.column()); int pos = -1; bool ok = false; if (posVar.isValid()) pos = posVar.toInt(&ok); if (ok && pos != -1) { QString r = result.toString(); r.insert(pos, Project::instance()->accel()); result = r; } } else if (item.column() == TMDBModel::Filepath) { return shorterFilePath(result.toString()); } else if (item.column() == TMDBModel::TransationStatus) { static QString statuses[] = {i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"), i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"), i18nc("@info:status", "Untranslated") }; return statuses[translationStatus(item)]; } if (doHtml && item.column() < TMDBModel::Context) return convertToHtml(result.toString(), item.column() == TMDBModel::Target && !rowIsApproved(item.row())); else return result; } //END TMDBModel //BEGIN TMResultsSortFilterProxyModel class TMResultsSortFilterProxyModel: public QSortFilterProxyModel { public: TMResultsSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} void setRules(const QVector& rules); void fetchMore(const QModelIndex& parent) override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; private: QVector m_rules; mutable QMap m_matchingRulesForSourceRow; //mutable QMap > m_highlightDataForSourceRow; }; bool TMResultsSortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { if (left.column() == TMDBModel::TransationStatus) { int l = left.data(TMDBModel::TransStateRole).toInt(); int r = right.data(TMDBModel::TransStateRole).toInt(); return l < r; } return QSortFilterProxyModel::lessThan(left, right); } void TMResultsSortFilterProxyModel::fetchMore(const QModelIndex& parent) { int oldSourceRowCount = sourceModel()->rowCount(); int oldRowCount = rowCount(); QSortFilterProxyModel::fetchMore(parent); if (m_rules.isEmpty()) return; while (oldRowCount == rowCount()) { QSortFilterProxyModel::fetchMore(parent); if (sourceModel()->rowCount() == oldSourceRowCount) break; oldSourceRowCount = sourceModel()->rowCount(); } qCDebug(LOKALIZE_LOG) << "row count" << sourceModel()->rowCount() << " filtered:" << rowCount(); emit layoutChanged(); } void TMResultsSortFilterProxyModel::setRules(const QVector& rules) { m_rules = rules; m_matchingRulesForSourceRow.clear(); invalidateFilter(); } QVariant TMResultsSortFilterProxyModel::data(const QModelIndex& index, int role) const { QVariant result = QSortFilterProxyModel::data(index, role); if (m_rules.isEmpty() || role != FastSizeHintItemDelegate::HtmlDisplayRole) return result; if (index.column() != TMDBModel::Source && index.column() != TMDBModel::Target) return result; int source_row = mapToSource(index).row(); QString string = result.toString(); QVector regExps; if (index.column() == TMDBModel::Source) regExps = m_rules[m_matchingRulesForSourceRow[source_row]].sources; else regExps = m_rules[m_matchingRulesForSourceRow[source_row]].falseFriends; foreach (const QRegExp& re, regExps) { int pos = re.indexIn(string); if (pos != -1) return string.replace(pos, re.matchedLength(), QStringLiteral("") % re.cap(0) % QStringLiteral("")); } //StartLen sl=m_highlightDataForSourceRow.value(source_row).at(index.column()); return result; } bool TMResultsSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (m_rules.isEmpty()) return true; QString source = sourceModel()->index(source_row, TMDBModel::Source, source_parent).data().toString(); QString target = sourceModel()->index(source_row, TMDBModel::Target, source_parent).data().toString(); static QVector dummy_positions; int i = findMatchingRule(m_rules, source, target, dummy_positions); bool accept = (i != -1); if (accept) m_matchingRulesForSourceRow[source_row] = i; return accept; } //END TMResultsSortFilterProxyModel class QueryStylesModel: public QStringListModel { public: explicit QueryStylesModel(QObject* parent = nullptr); QVariant data(const QModelIndex& item, int role) const override; }; QueryStylesModel::QueryStylesModel(QObject* parent): QStringListModel(parent) { setStringList(QStringList(i18n("Substring")) << i18n("Google-like") << i18n("Wildcard")); } QVariant QueryStylesModel::data(const QModelIndex& item, int role) const { if (role == Qt::ToolTipRole) { static QString tooltips[] = {i18n("Case insensitive"), i18n("Space is AND operator. Case insensitive."), i18n("Shell globs (* and ?). Case sensitive.") }; return tooltips[item.row()]; } return QStringListModel::data(item, role); } //BEGIN TMWindow TMTab::TMTab(QWidget *parent) : LokalizeSubwindowBase2(parent) , m_proxyModel(new TMResultsSortFilterProxyModel(this)) , m_partToAlsoTryLater(DocPosition::UndefPart) , m_dbusId(-1) { //setCaption(i18nc("@title:window","Translation Memory"),false); setWindowTitle(i18nc("@title:window", "Translation Memory")); setAcceptDrops(true); ui_queryOptions = new Ui_QueryOptions; QWidget* w = new QWidget(this); ui_queryOptions->setupUi(w); setCentralWidget(w); ui_queryOptions->queryLayout->setStretchFactor(ui_queryOptions->mainQueryLayout, 42); connect(ui_queryOptions->querySource, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->queryTarget, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->filemask, &QLineEdit::returnPressed, this, &TMTab::performQuery); connect(ui_queryOptions->doFind, &QPushButton::clicked, this, &TMTab::performQuery); connect(ui_queryOptions->doUpdateTM, &QPushButton::clicked, this, &TMTab::updateTM); QShortcut* sh = new QShortcut(Qt::CTRL + Qt::Key_L, this); connect(sh, &QShortcut::activated, ui_queryOptions->querySource, QOverload<>::of(&QLineEdit::setFocus)); setFocusProxy(ui_queryOptions->querySource); QTreeView* view = ui_queryOptions->treeView; view->setContextMenuPolicy(Qt::ActionsContextMenu); QAction* a = new QAction(i18n("Copy source to clipboard"), view); a->setShortcut(Qt::CTRL + Qt::Key_S); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::copySource); view->addAction(a); a = new QAction(i18n("Copy target to clipboard"), view); a->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::copyTarget); view->addAction(a); a = new QAction(i18n("Open file"), view); a->setShortcut(QKeySequence(Qt::Key_Return)); a->setShortcutContext(Qt::WidgetWithChildrenShortcut); connect(a, &QAction::triggered, this, &TMTab::openFile); connect(view, &QTreeView::activated, this, &TMTab::openFile); view->addAction(a); //view->addAction(KStandardAction::copy(this),this,SLOT(),this); //QKeySequence::Copy? //QShortcut* shortcut = new QShortcut(Qt::CTRL + Qt::Key_P,view,0,0,Qt::WidgetWithChildrenShortcut); //connect(shortcut,SIGNAL(activated()), this, SLOT(copyText())); m_model = new TMDBModel(this); m_model->setDB(Project::instance()->projectID()); m_proxyModel->setDynamicSortFilter(true); m_proxyModel->setSourceModel(m_model); view->setModel(m_proxyModel); view->sortByColumn(TMDBModel::Filepath, Qt::AscendingOrder); view->setSortingEnabled(true); view->setColumnHidden(TMDBModel::_SourceAccel, true); view->setColumnHidden(TMDBModel::_TargetAccel, true); view->setColumnHidden(TMDBModel::_Bits, true); QVector singleLineColumns(TMDBModel::ColumnCount, false); singleLineColumns[TMDBModel::Filepath] = true; singleLineColumns[TMDBModel::TransationStatus] = true; singleLineColumns[TMDBModel::Context] = true; QVector richTextColumns(TMDBModel::ColumnCount, false); richTextColumns[TMDBModel::Source] = true; richTextColumns[TMDBModel::Target] = true; view->setItemDelegate(new FastSizeHintItemDelegate(this, singleLineColumns, richTextColumns)); connect(m_model, &TMDBModel::resultsFetched, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_model, &TMDBModel::modelReset, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); //connect(m_model,SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),view->itemDelegate(),SLOT(reset())); connect(m_proxyModel, &TMResultsSortFilterProxyModel::layoutChanged, (FastSizeHintItemDelegate*)view->itemDelegate(), &FastSizeHintItemDelegate::reset); connect(m_proxyModel, &TMResultsSortFilterProxyModel::layoutChanged, this, &TMTab::displayTotalResultCount); connect(m_model, &TMDBModel::resultsFetched, this, &TMTab::handleResults); connect(m_model, &TMDBModel::finalResultCountFetched, this, &TMTab::displayTotalResultCount); ui_queryOptions->queryStyle->setModel(new QueryStylesModel(this)); connect(ui_queryOptions->queryStyle, QOverload::of(&KComboBox::currentIndexChanged), m_model, &TMDBModel::setQueryType); ui_queryOptions->dbName->setModel(DBFilesModel::instance()); ui_queryOptions->dbName->setRootModelIndex(DBFilesModel::instance()->rootIndex()); int pos = ui_queryOptions->dbName->findData(Project::instance()->projectID(), DBFilesModel::NameRole); if (pos >= 0) ui_queryOptions->dbName->setCurrentIndex(pos); connect(ui_queryOptions->dbName, QOverload::of(&QComboBox::currentIndexChanged), m_model, &TMDBModel::setDB); //connect(ui_queryOptions->dbName, SIGNAL(activated(QString)), this, SLOT(performQuery())); //BEGIN resizeColumnToContents static const int maxInitialWidths[4] = {QApplication::desktop()->availableGeometry().width() / 3, QApplication::desktop()->availableGeometry().width() / 3, 50, 200}; int column = sizeof(maxInitialWidths) / sizeof(int); while (--column >= 0) view->setColumnWidth(column, maxInitialWidths[column]); //END resizeColumnToContents int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); setXMLFile(QStringLiteral("translationmemoryrui.rc"), true); setUpdatedXMLFile(); dbusObjectPath(); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* tm = new KActionCategory(i18nc("@title actions category", "Translation Memory"), ac); action = tm->addAction(QStringLiteral("tools_tm_manage"), Project::instance(), SLOT(showTMManager())); action->setText(i18nc("@action:inmenu", "Manage translation memories")); m_qaView = new QaView(this); m_qaView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_qaView); tm->addAction(QStringLiteral("showqa_action"), m_qaView->toggleViewAction()); connect(m_qaView, &QaView::rulesChanged, this, QOverload<>::of(&TMTab::setQAMode)); connect(m_qaView->toggleViewAction(), &QAction::toggled, this, QOverload::of(&TMTab::setQAMode)); KConfig config; KConfigGroup cg(&config, "MainWindow"); view->header()->restoreState(QByteArray::fromBase64(cg.readEntry("TMSearchResultsHeaderState", QByteArray()))); } TMTab::~TMTab() { KConfig config; KConfigGroup cg(&config, "MainWindow"); cg.writeEntry("TMSearchResultsHeaderState", ui_queryOptions->treeView->header()->saveState().toBase64()); ids.removeAll(m_dbusId); delete ui_queryOptions; } void TMTab::updateTM() { scanRecursive(QStringList(Project::instance()->poDir()), Project::instance()->projectID()); if (Settings::deleteFromTMOnMissing()) { RemoveMissingFilesJob* job = new RemoveMissingFilesJob(Project::instance()->projectID()); TM::threadPool()->start(job, REMOVEMISSINGFILES); } } void TMTab::performQuery() { if (ui_queryOptions->dbName->currentText().isEmpty()) { int pos = ui_queryOptions->dbName->findData(Project::instance()->projectID(), DBFilesModel::NameRole); if (pos >= 0) ui_queryOptions->dbName->setCurrentIndex(pos); //m_model->setDB(Project::instance()->projectID()); } m_model->m_dbOperationMutex.lock(); m_model->setFilter(ui_queryOptions->querySource->text(), ui_queryOptions->queryTarget->text(), ui_queryOptions->invertSource->isChecked(), ui_queryOptions->invertTarget->isChecked(), ui_queryOptions->filemask->text() ); m_model->m_dbOperationMutex.unlock(); QApplication::setOverrideCursor(Qt::BusyCursor); } void TMTab::handleResults() { QApplication::restoreOverrideCursor(); QString filemask = ui_queryOptions->filemask->text(); //ui_queryOptions->regexSource->text(),ui_queryOptions->regexTarget->text() m_model->m_dbOperationMutex.lock(); int rowCount = m_model->rowCount(); m_model->m_dbOperationMutex.unlock(); if (rowCount == 0) { qCDebug(LOKALIZE_LOG) << "m_model->rowCount()==0"; //try harder if (m_partToAlsoTryLater != DocPosition::UndefPart) { if (m_partToAlsoTryLater == DocPosition::Comment) { QString text = ui_queryOptions->queryTarget->text(); if (text.isEmpty()) text = ui_queryOptions->querySource->text(); if (text.isEmpty()) m_partToAlsoTryLater = DocPosition::UndefPart; else findGuiText(text); return; } QLineEdit* const source_target_query[] = {ui_queryOptions->queryTarget, ui_queryOptions->querySource}; source_target_query[m_partToAlsoTryLater == DocPosition::Source]->setText(source_target_query[m_partToAlsoTryLater != DocPosition::Source]->text()); source_target_query[m_partToAlsoTryLater != DocPosition::Source]->clear(); m_partToAlsoTryLater = ui_queryOptions->filemask->text().isEmpty() ? DocPosition::UndefPart : DocPosition::Comment; //leave a note that we should also try w/o package if the current one doesn't succeed return performQuery(); } if (!filemask.isEmpty() && !filemask.contains('*')) { ui_queryOptions->filemask->setText('*' % filemask % '*'); return performQuery(); } } qCDebug(LOKALIZE_LOG) << "=DocPosition::UndefPart"; m_partToAlsoTryLater = DocPosition::UndefPart; ui_queryOptions->treeView->setFocus(); } void TMTab::displayTotalResultCount() { m_model->m_dbOperationMutex.lock(); int total = m_model->totalResultCount(); int filtered = m_proxyModel->rowCount(); if (filtered == m_model->rowCount()) statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1", total)); else statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1 (%2)", filtered, total)); m_model->m_dbOperationMutex.unlock(); } static void copy(Ui_QueryOptions* ui_queryOptions, int column) { QApplication::clipboard()->setText(ui_queryOptions->treeView->currentIndex().sibling(ui_queryOptions->treeView->currentIndex().row(), column).data().toString()); } void TMTab::copySource() { copy(ui_queryOptions, TMDBModel::Source); } void TMTab::copyTarget() { copy(ui_queryOptions, TMDBModel::Target); } void TMTab::openFile() { QModelIndex item = ui_queryOptions->treeView->currentIndex(); if (Settings::deleteFromTMOnMissing()) { //Check if the file exists and delete it if it doesn't QString filePath = item.sibling(item.row(), TMDBModel::Filepath).data(Qt::UserRole).toString(); if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(filePath, ui_queryOptions->dbName->currentText()); TM::threadPool()->start(job, REMOVEFILE); KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", filePath)); return performQuery();//We relaunch the query } } emit fileOpenRequested(item.sibling(item.row(), TMDBModel::Filepath).data(Qt::UserRole).toString(), item.sibling(item.row(), TMDBModel::Source).data().toString(), item.sibling(item.row(), TMDBModel::Context).data().toString(), true); } void TMTab::setQAMode() { return setQAMode(true); } void TMTab::setQAMode(bool enable) { static_cast(ui_queryOptions->treeView->itemDelegate())->reset(); if (!enable) { m_proxyModel->setRules(QVector()); return; } m_proxyModel->setRules(m_qaView->rules()); /*QDomElement docElem = m_categories.at(0).toElement(); QDomNode n = docElem.firstChildElement(); while(!n.isNull()) { QDomElement e = n.toElement(); qCDebug(LOKALIZE_LOG) << e.tagName(); n = n.nextSiblingElement(); }*/ performQuery(); } //END TMWindow #if 0 bool QueryResultDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { qCWarning(LOKALIZE_LOG) << "QEvent" << event; if (event->type() == QEvent::Shortcut) { qCWarning(LOKALIZE_LOG) << "QEvent::Shortcut" << index.data().canConvert(QVariant::String); if (static_cast(event)->key().matches(QKeySequence::Copy) && index.data().canConvert(QVariant::String)) { QApplication::clipboard()->setText(index.data().toString()); qCWarning(LOKALIZE_LOG) << "index.data().toString()"; } } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mEvent = static_cast(event); if (mEvent->button() == Qt::MidButton) { } } else if (event->type() == QEvent::KeyPress) { QKeyEvent* kEvent = static_cast(event); if (kEvent->key() == Qt::Key_Return) { if (kEvent->modifiers() == Qt::NoModifier) { } } } else return false; event->accept(); return true; } #endif void TMTab::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMTab::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } #include "translationmemoryadaptor.h" //BEGIN DBus interface QList TMTab::ids; QString TMTab::dbusObjectPath() { const QString TM_PATH = QStringLiteral("/ThisIsWhatYouWant/TranslationMemory/"); if (m_dbusId == -1) { new TranslationMemoryAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(TM_PATH + QString::number(m_dbusId), this); } return TM_PATH + QString::number(m_dbusId); } void TMTab::lookup(QString source, QString target) { source.remove(Project::instance()->accel()); target.remove(Project::instance()->accel()); ui_queryOptions->querySource->setText(source); ui_queryOptions->queryTarget->setText(target); ui_queryOptions->invertSource->setChecked(false); ui_queryOptions->invertTarget->setChecked(false); ui_queryOptions->queryStyle->setCurrentIndex(TMDBModel::SubStr); performQuery(); } // void TMTab::lookup(DocPosition::Part part, QString text) // { // lookup(part==DocPosition::Source?text:QString(),part==DocPosition::Target?text:QString()); // } bool TMTab::findGuiTextPackage(QString text, QString package) { qCWarning(LOKALIZE_LOG) << package << text; QLineEdit* const source_target_query[] = {ui_queryOptions->queryTarget, ui_queryOptions->querySource}; static const DocPosition::Part source_target[] = {DocPosition::Target, DocPosition::Source}; QTextCodec* latin1 = QTextCodec::codecForMib(4); DocPosition::Part tryNowPart = source_target[latin1->canEncode(text)]; m_partToAlsoTryLater = source_target[tryNowPart == DocPosition::Target]; text.remove(Project::instance()->accel()); ui_queryOptions->querySource->clear(); ui_queryOptions->queryTarget->clear(); source_target_query[tryNowPart == DocPosition::Source]->setText(text); ui_queryOptions->invertSource->setChecked(false); ui_queryOptions->invertTarget->setChecked(false); if (!package.isEmpty()) package = '*' % package % '*'; ui_queryOptions->filemask->setText(package); ui_queryOptions->queryStyle->setCurrentIndex(TMDBModel::Glob); performQuery(); return true; } //END DBus interface diff --git a/src/tm/tmtab.h b/src/tm/tmtab.h index 5496b27..673e5fd 100644 --- a/src/tm/tmtab.h +++ b/src/tm/tmtab.h @@ -1,193 +1,194 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #ifndef TMTAB_H #define TMTAB_H #include "lokalizesubwindowbase.h" #include "pos.h" #include #include #include #include class KXMLGUIClient; class QComboBox; class QTreeView; class QSortFilterProxyModel; class QCheckBox; class QaView; class Ui_QueryOptions; class TMResultsSortFilterProxyModel; namespace TM { class TMDBModel; class ExecQueryJob; /** * Translation Memory tab */ class TMTab: public LokalizeSubwindowBase2 { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Lokalize.TranslationMemory") //qdbuscpp2xml -m -s tm/tmtab.h -o tm/org.kde.lokalize.TranslationMemory.xml public: explicit TMTab(QWidget *parent); ~TMTab() override; void hideDocks() override {} void showDocks() override {} KXMLGUIClient* guiClient() override { return (KXMLGUIClient*)this; } QString dbusObjectPath(); int dbusId() { return m_dbusId; } public slots: Q_SCRIPTABLE bool findGuiText(QString text) { return findGuiTextPackage(text, QString()); } Q_SCRIPTABLE bool findGuiTextPackage(QString text, QString package); Q_SCRIPTABLE void lookup(QString source, QString target); //void lookup(DocPosition::Part, QString text); public slots: void performQuery(); void updateTM(); void copySource(); void copyTarget(); void openFile(); void handleResults(); void displayTotalResultCount(); void setQAMode(); void setQAMode(bool enabled); signals: void fileOpenRequested(const QString& url, const QString& source, const QString& ctxt, const bool setAsActive); private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent*) override; private: Ui_QueryOptions* ui_queryOptions; TMDBModel* m_model; TMResultsSortFilterProxyModel *m_proxyModel; QaView* m_qaView; DocPosition::Part m_partToAlsoTryLater; int m_dbusId; static QList ids; }; class TMDBModel: public QSqlQueryModel { Q_OBJECT public: enum TMDBModelColumns { Source = 0, Target, Context, Filepath, _SourceAccel, _TargetAccel, _Bits, TransationStatus, ColumnCount }; enum QueryType { SubStr = 0, WordOrder, Glob }; enum Roles { FullPathRole = Qt::UserRole, TransStateRole = Qt::UserRole + 1, //HtmlDisplayRole=FastSizeHintItemDelegate::HtmlDisplayRole }; explicit TMDBModel(QObject* parent); ~TMDBModel() {} QVariant data(const QModelIndex& item, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override { Q_UNUSED(parent); return ColumnCount; } int totalResultCount()const { return m_totalResultCount; } QString dbName()const { return m_dbName; } public slots: void setFilter(const QString& source, const QString& target, bool invertSource, bool invertTarget, const QString& filemask ); void setQueryType(int); void setDB(const QString&); void slotQueryExecuted(ExecQueryJob*); signals: void resultsFetched(); void finalResultCountFetched(int); private: bool rowIsApproved(int row) const; int translationStatus(const QModelIndex& item) const; private: QueryType m_queryType; QString m_dbName; int m_totalResultCount; public: mutable QMutex m_dbOperationMutex; }; //const QString& sourceRefine, const QString& targetRefine } #endif diff --git a/src/tm/tmview.cpp b/src/tm/tmview.cpp index 0f9ba45..e67a61b 100644 --- a/src/tm/tmview.cpp +++ b/src/tm/tmview.cpp @@ -1,1028 +1,1029 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "tmview.h" #include "lokalize_debug.h" #include "jobs.h" #include "tmscanapi.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "dbfilesmodel.h" #include "diff.h" #include "xlifftextedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef NDEBUG #undef NDEBUG #endif #define DEBUG using namespace TM; struct DiffInfo { DiffInfo(int reserveSize); QString diffClean; QString old; //Formatting info: QByteArray diffIndex; //Map old string-->d.diffClean QVector old2DiffClean; }; DiffInfo::DiffInfo(int reserveSize) { diffClean.reserve(reserveSize); old.reserve(reserveSize); diffIndex.reserve(reserveSize); old2DiffClean.reserve(reserveSize); } /** * 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 (M appears afterwards) */ static DiffInfo getDiffInfo(const QString& diff) { DiffInfo d(diff.size()); QChar sep('{'); char state = '0'; //walk through diff string char-by-char //calculate old and others int pos = -1; while (++pos < diff.size()) { if (diff.at(pos) == sep) { if (diff.indexOf(QLatin1String("{KBABELDEL}"), pos) == pos) { state = '-'; pos += 10; } else if (diff.indexOf(QLatin1String("{KBABELADD}"), pos) == pos) { state = '+'; pos += 10; } else if (diff.indexOf(QLatin1String("{/KBABEL"), pos) == pos) { state = '0'; pos += 11; } } else { if (state != '+') { d.old.append(diff.at(pos)); d.old2DiffClean.append(d.diffIndex.count()); } d.diffIndex.append(state); d.diffClean.append(diff.at(pos)); } } return d; } void TextBrowser::mouseDoubleClickEvent(QMouseEvent* event) { QTextBrowser::mouseDoubleClickEvent(event); QString sel = textCursor().selectedText(); if (!(sel.isEmpty() || sel.contains(' '))) emit textInsertRequested(sel); } TMView::TMView(QWidget* parent, Catalog* catalog, const QVector& actions_insert, const QVector& actions_remove) : QDockWidget(i18nc("@title:window", "Translation Memory"), parent) , m_browser(new TextBrowser(this)) , m_catalog(catalog) , m_currentSelectJob(0) , m_actions_insert(actions_insert) , m_actions_remove(actions_remove) , m_normTitle(i18nc("@title:window", "Translation Memory")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_isBatching(false) , m_markAsFuzzy(false) { setObjectName(QStringLiteral("TMView")); setWidget(m_browser); m_browser->document()->setDefaultStyleSheet(QStringLiteral("p.close_match { font-weight:bold; }")); m_browser->viewport()->setBackgroundRole(QPalette::Background); QTimer::singleShot(0, this, &TMView::initLater); connect(m_catalog, QOverload::of(&Catalog::signalFileLoaded), this, &TMView::slotFileLoaded); } TMView::~TMView() { #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_jobs.at(i)); #endif } void TMView::initLater() { setAcceptDrops(true); QSignalMapper* signalMapper_insert = new QSignalMapper(this); QSignalMapper* signalMapper_remove = new QSignalMapper(this); int i = m_actions_insert.size(); while (--i >= 0) { connect(m_actions_insert.at(i), &QAction::triggered, signalMapper_insert, QOverload<>::of(&QSignalMapper::map)); signalMapper_insert->setMapping(m_actions_insert.at(i), i); } i = m_actions_remove.size(); while (--i >= 0) { connect(m_actions_remove.at(i), &QAction::triggered, signalMapper_remove, QOverload<>::of(&QSignalMapper::map)); signalMapper_remove->setMapping(m_actions_remove.at(i), i); } connect(signalMapper_insert, QOverload::of(&QSignalMapper::mapped), this, &TMView::slotUseSuggestion); connect(signalMapper_remove, QOverload::of(&QSignalMapper::mapped), this, &TMView::slotRemoveSuggestion); setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); DBFilesModel::instance(); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &TMView::textInsertRequested); connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &TMView::contextMenu); //TODO ? kdisplayPaletteChanged // connect(KGlobalSettings::self(),,SIGNAL(kdisplayPaletteChanged()),this,SLOT(slotPaletteChanged())); } void TMView::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMView::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } void TMView::slotFileLoaded(const QString& filePath) { const QString& pID = Project::instance()->projectID(); if (Settings::scanToTMOnOpen()) TM::threadPool()->start(new ScanJob(filePath, pID), SCAN); if (!Settings::prefetchTM() && !m_isBatching) return; m_cache.clear(); #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_jobs.at(i)); #endif m_jobs.clear(); DocPosition pos; while (switchNext(m_catalog, pos)) { if (!m_catalog->isEmpty(pos.entry) && m_catalog->isApproved(pos.entry)) continue; SelectJob* j = initSelectJob(m_catalog, pos, pID); connect(j, &SelectJob::done, this, &TMView::slotCacheSuggestions); m_jobs.append(j); } //dummy job for the finish indication BatchSelectFinishedJob* m_seq = new BatchSelectFinishedJob(this); connect(m_seq, &BatchSelectFinishedJob::done, this, &TMView::slotBatchSelectDone); TM::threadPool()->start(m_seq, BATCHSELECTFINISHED); m_jobs.append(m_seq); } void TMView::slotCacheSuggestions(SelectJob* job) { m_jobs.removeAll(job); qCDebug(LOKALIZE_LOG) << job->m_pos.entry; if (job->m_pos.entry == m_pos.entry) slotSuggestionsCame(job); m_cache[DocPos(job->m_pos)] = job->m_entries.toVector(); } void TMView::slotBatchSelectDone() { m_jobs.clear(); if (!m_isBatching) return; bool insHappened = false; DocPosition pos; while (switchNext(m_catalog, pos)) { if (!(m_catalog->isEmpty(pos.entry) || !m_catalog->isApproved(pos.entry)) ) continue; const QVector& suggList = m_cache.value(DocPos(pos)); if (suggList.isEmpty()) continue; const TMEntry& entry = suggList.first(); if (entry.score < 9900) //hacky continue; { bool forceFuzzy = (suggList.size() > 1 && suggList.at(1).score >= 10000) || entry.score < 10000; bool ctxtMatches = entry.score == 1001; if (!m_catalog->isApproved(pos.entry)) { ///m_catalog->push(new DelTextCmd(m_catalog,pos,m_catalog->msgstr(pos))); removeTargetSubstring(m_catalog, pos, 0, m_catalog->targetWithTags(pos).string.size()); if (ctxtMatches || !(m_markAsFuzzy || forceFuzzy)) SetStateCmd::push(m_catalog, pos, true); } else if ((m_markAsFuzzy && !ctxtMatches) || forceFuzzy) { SetStateCmd::push(m_catalog, pos, false); } ///m_catalog->push(new InsTextCmd(m_catalog,pos,entry.target)); insertCatalogString(m_catalog, pos, entry.target, 0); if (Q_UNLIKELY(m_pos.entry == pos.entry && pos.form == m_pos.form)) emit refreshRequested(); } if (!insHappened) { insHappened = true; m_catalog->beginMacro(i18nc("@item Undo action", "Batch translation memory filling")); } } QString msg = i18nc("@info", "Batch translation has been completed."); if (insHappened) m_catalog->endMacro(); else { // xgettext: no-c-format msg += ' '; msg += i18nc("@info", "No suggestions with exact matches were found."); } KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation complete"), msg, this); } void TMView::slotBatchTranslate() { m_isBatching = true; m_markAsFuzzy = false; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) return slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotBatchTranslateFuzzy() { m_isBatching = true; m_markAsFuzzy = true; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotNewEntryDisplayed() { return slotNewEntryDisplayed(DocPosition()); } void TMView::slotNewEntryDisplayed(const DocPosition& pos) { if (m_catalog->numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_currentSelectJob); #endif //update DB //m_catalog->flushUpdateDBBuffer(); //this is called via subscribtion if (pos.entry != -1) m_pos = pos; m_browser->clear(); if (Settings::prefetchTM() && m_cache.contains(DocPos(m_pos))) { QTimer::singleShot(0, this, &TMView::displayFromCache); } m_currentSelectJob = initSelectJob(m_catalog, m_pos); connect(m_currentSelectJob, &TM::SelectJob::done, this, &TMView::slotSuggestionsCame); } void TMView::displayFromCache() { if (m_prevCachePos.entry == m_pos.entry && m_prevCachePos.form == m_pos.form) return; SelectJob* temp = initSelectJob(m_catalog, m_pos, QString(), 0); temp->m_entries = m_cache.value(DocPos(m_pos)).toList(); slotSuggestionsCame(temp); temp->deleteLater(); m_prevCachePos = m_pos; } void TMView::slotSuggestionsCame(SelectJob* j) { QTime time; time.start(); SelectJob& job = *j; job.deleteLater(); if (job.m_pos.entry != m_pos.entry) return; Catalog& catalog = *m_catalog; if (catalog.numberOfEntries() <= m_pos.entry) return;//because of Qt::QueuedConnection //BEGIN query other DBs handling Project* project = Project::instance(); const QString& projectID = project->projectID(); //check if this is an additional query, from secondary DBs if (job.m_dbName != projectID) { job.m_entries += m_entries; qSort(job.m_entries.begin(), job.m_entries.end(), qGreater()); const int limit = qMin(Settings::suggCount(), job.m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = job.m_entries.size() - 1; while (i >= 0 && (i >= limit || job.m_entries.last().score < minScore)) { job.m_entries.removeLast(); i--; } } else if (job.m_entries.isEmpty() || job.m_entries.first().score < 8500) { //be careful, as we switched to QDirModel! DBFilesModel& dbFilesModel = *(DBFilesModel::instance()); QModelIndex root = dbFilesModel.rootIndex(); int i = dbFilesModel.rowCount(root); //qCWarning(LOKALIZE_LOG)<<"query other DBs,"<= 0) { const QString& dbName = dbFilesModel.data(dbFilesModel.index(i, 0, root), DBFilesModel::NameRole).toString(); if (projectID != dbName && dbFilesModel.m_configurations.value(dbName).targetLangCode == catalog.targetLangCode()) { SelectJob* j = initSelectJob(m_catalog, m_pos, dbName); connect(j, &SelectJob::done, this, &TMView::slotSuggestionsCame); m_jobs.append(j); } } } //END query other DBs handling m_entries = job.m_entries; const int limit = job.m_entries.size(); if (!limit) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(false); m_browser->clear(); m_entryPositions.clear(); //m_entries=job.m_entries; //m_browser->insertHtml(""); int i = 0; QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); forever { QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); const TMEntry& entry = job.m_entries.at(i); html += (entry.score > 9500) ? QStringLiteral("

    ") : QStringLiteral("

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

    ") : QStringLiteral("

    "); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } m_browser->insertHtml(QStringLiteral("")); setUpdatesEnabled(true); // qCWarning(LOKALIZE_LOG)<<"ELA "<document()->blockCount(); } /* void TMView::slotPaletteChanged() { }*/ bool TMView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); //int block1=m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); QMap::iterator block = m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block != m_entryPositions.end() && *block < m_entries.size()) { const TMEntry& tmEntry = m_entries.at(*block); QString file = tmEntry.file; if (file == m_catalog->url()) file = i18nc("File argument in tooltip, when file is current file", "this"); QString tooltip = i18nc("@info:tooltip", "File: %1
    Addition date: %2", file, tmEntry.date.toString(Qt::ISODate)); if (!tmEntry.changeDate.isNull() && tmEntry.changeDate != tmEntry.date) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Last change date: %1", tmEntry.changeDate.toString(Qt::ISODate)); if (!tmEntry.changeAuthor.isEmpty()) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Last change author: %1", tmEntry.changeAuthor); tooltip += i18nc("@info:tooltip on TM entry continues", "
    TM: %1", tmEntry.dbName); if (tmEntry.obsolete) tooltip += i18nc("@info:tooltip on TM entry continues", "
    Is not present in the file anymore"); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } } return QWidget::event(event); } void TMView::removeEntry(const TMEntry& e) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this entry:
    %1
    from translation memory %2?", e.target.string.toHtmlEscaped(), e.dbName), i18nc("@title:window", "Translation Memory Entry Removal"))) { RemoveJob* job = new RemoveJob(e); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVE); } } void TMView::deleteFile(const TMEntry& e, const bool showPopUp) { QString filePath = e.file; if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(e.file, e.dbName); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVEFILE); if (showPopUp) { KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", e.file)); } return; } } void TMView::contextMenu(const QPoint& pos) { int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(pos).anchor()); qCWarning(LOKALIZE_LOG) << block; if (block >= m_entries.size()) return; const TMEntry& e = m_entries.at(block); enum {Remove, RemoveFile, Open}; QMenu popup; popup.addAction(i18nc("@action:inmenu", "Remove this entry"))->setData(Remove); if (e.file != m_catalog->url() && QFile::exists(e.file)) popup.addAction(i18nc("@action:inmenu", "Open file containing this entry"))->setData(Open); else { if (Settings::deleteFromTMOnMissing()) { //Automatic deletion deleteFile(e, true); } else if (!QFile::exists(e.file)) { //Still offer manual deletion if this is not the current file popup.addAction(i18nc("@action:inmenu", "Remove this missing file from TM"))->setData(RemoveFile); } } QAction* r = popup.exec(m_browser->mapToGlobal(pos)); if (!r) return; if (r->data().toInt() == Remove) { removeEntry(e); } else if (r->data().toInt() == Open) { emit fileOpenRequested(e.file, e.source.string, e.ctxt, true); } else if ((r->data().toInt() == RemoveFile) && KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this missing file:
    %1
    from translation memory %2?", e.file, e.dbName), i18nc("@title:window", "Translation Memory Missing File Removal"))) { deleteFile(e, false); } } /** * helper function: * searches to th nearest rxNum or ABBR * clears rxNum if ABBR is found before rxNum */ static int nextPlacableIn(const QString& old, int start, QString& cap) { static QRegExp rxNum(QStringLiteral("[\\d\\.\\%]+")); static QRegExp rxAbbr(QStringLiteral("\\w+")); int numPos = rxNum.indexIn(old, start); // int abbrPos=rxAbbr.indexIn(old,start); int abbrPos = start; //qCWarning(LOKALIZE_LOG)<<"seeing"<= 0) { if ((c++)->isUpper()) break; } abbrPos += rxAbbr.matchedLength(); } int pos = qMin(numPos, abbrPos); if (pos == -1) pos = qMax(numPos, abbrPos); // if (pos==numPos) // cap=rxNum.cap(0); // else // cap=rxAbbr.cap(0); cap = (pos == numPos ? rxNum : rxAbbr).cap(0); //qCWarning(LOKALIZE_LOG)<]*") % Settings::addColor().name() % QLatin1String("[^>]*\">([^>]*)")); QRegExp rxDel(QLatin1String("]*") % Settings::delColor().name() % QLatin1String("[^>]*\">([^>]*)")); //rxAdd.setMinimal(true); //rxDel.setMinimal(true); //first things first int pos = 0; while ((pos = rxDel.indexIn(diff, pos)) != -1) diff.replace(pos, rxDel.matchedLength(), "\tKBABELDEL\t" % rxDel.cap(1) % "\t/KBABELDEL\t"); pos = 0; while ((pos = rxAdd.indexIn(diff, pos)) != -1) diff.replace(pos, rxAdd.matchedLength(), "\tKBABELADD\t" % rxAdd.cap(1) % "\t/KBABELADD\t"); diff.replace(QStringLiteral("<"), QStringLiteral("<")); diff.replace(QStringLiteral(">"), QStringLiteral(">")); //possible enhancement: search for non-translated words in removedSubstrings... //QStringList removedSubstrings; //QStringList addedSubstrings; /* 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 */ DiffInfo d = getDiffInfo(diff); bool sameMarkup = Project::instance()->markup() == entry.markupExpr && !entry.markupExpr.isEmpty(); bool tryMarkup = !entry.target.tags.size() && sameMarkup; //search for changed markup if (tryMarkup) { QRegExp rxMarkup(entry.markupExpr); rxMarkup.setMinimal(true); pos = 0; int replacingPos = 0; while ((pos = rxMarkup.indexIn(d.old, pos)) != -1) { //qCWarning(LOKALIZE_LOG)<<"size"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 0) { QByteArray diffMPart(d.diffIndex.left(len)); int m = diffMPart.indexOf('M'); if (m != -1) diffMPart.truncate(m); #if 0 nono //first goes del, then add. so stop on second del sequence bool seenAdd = false; int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) == '+') seenAdd = true; else if (seenAdd && diffMPart.at(j) == '-') { diffMPart.truncate(j); break; } } #endif //form 'oldMarkup' QString oldMarkup; oldMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '+') oldMarkup.append(d.diffClean.at(j)); } //qCWarning(LOKALIZE_LOG)<<"old"<= 0) d.diffIndex[j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= 0) d.diffIndex[len + j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 500 cases while ((++endPos < d.diffIndex.size()) && (d.diffIndex.at(endPos) == '+') && (-1 != nextPlacableIn(QString(d.diffClean.at(endPos)), 0, _)) ) diffMPart.append('+'); qCDebug(LOKALIZE_LOG) << "diffMPart extended 1" << diffMPart; // if ((pos-1>=0) && (d.old2DiffClean.at(pos)>=0)) // { // qCWarning(LOKALIZE_LOG)<<"d.diffIndex"<= 0) && (d.diffIndex.at(startPos) == '+') //&&(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_)) ) diffMPart.prepend('+'); ++startPos; qCDebug(LOKALIZE_LOG) << "diffMPart extended 2" << diffMPart; if ((diffMPart.contains('-') || diffMPart.contains('+')) && (!diffMPart.contains('M'))) { //form newMarkup QString newMarkup; newMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '-') newMarkup.append(d.diffClean.at(startPos + j)); } if (newMarkup.endsWith(' ')) newMarkup.chop(1); //qCWarning(LOKALIZE_LOG)<<"d.old"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= m_entries.size())) return; const TMEntry& e = m_entries.at(i); removeEntry(e); } void TMView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; CatalogString target = targetAdapted(m_entries.at(i), m_catalog->sourceWithTags(m_pos)); #if 0 QString tmp = target.string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, '*'); qCWarning(LOKALIZE_LOG) << "targetAdapted" << tmp; foreach (InlineTag tag, target.tags) qCWarning(LOKALIZE_LOG) << "tag" << tag.start << tag.end; #endif if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use translation memory suggestion")); QString old = m_catalog->targetWithTags(m_pos).string; if (!old.isEmpty()) { m_pos.offset = 0; //FIXME test! removeTargetSubstring(m_catalog, m_pos, 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_pos, target, 0); if (m_entries.at(i).score > 9900 && !m_catalog->isApproved(m_pos.entry)) SetStateCmd::push(m_catalog, m_pos, true); m_catalog->endMacro(); emit refreshRequested(); } diff --git a/src/tm/tmview.h b/src/tm/tmview.h index 6c718e7..c95c1fe 100644 --- a/src/tm/tmview.h +++ b/src/tm/tmview.h @@ -1,134 +1,135 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef TMVIEW_H #define TMVIEW_H #include "pos.h" #include "tmentry.h" #include #include #include #include class QRunnable; class Catalog; class QDropEvent; class QDragEnterEvent; #define TM_SHORTCUTS 10 namespace TM { class TextBrowser; class SelectJob; class TMView: public QDockWidget { Q_OBJECT public: explicit TMView(QWidget*, Catalog*, const QVector&, const QVector&); ~TMView() override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent*) override; QSize sizeHint() const override { return QSize(300, 100); } signals: // void textReplaceRequested(const QString&); void refreshRequested(); void textInsertRequested(const QString&); void fileOpenRequested(const QString& filePath, const QString& str, const QString& ctxt, const bool setAsActive); public slots: void slotNewEntryDisplayed(); void slotNewEntryDisplayed(const DocPosition& pos); void slotSuggestionsCame(SelectJob*); void slotUseSuggestion(int); void slotRemoveSuggestion(int); void slotFileLoaded(const QString& url); void displayFromCache(); void slotBatchTranslate(); void slotBatchTranslateFuzzy(); private slots: //i think we do not wanna cache suggestions: //what if good sugg may be generated //from the entry user translated 1 minute ago? void slotBatchSelectDone(); void slotCacheSuggestions(SelectJob*); void initLater(); void contextMenu(const QPoint & pos); void removeEntry(const TMEntry & e); private: bool event(QEvent *event) override; void deleteFile(const TMEntry& e, const bool showPopUp); private: TextBrowser* m_browser; Catalog* m_catalog; DocPosition m_pos; SelectJob* m_currentSelectJob; QVector m_actions_insert;//need them to get insertion shortcuts QVector m_actions_remove;//need them to get deletion shortcuts QList m_entries; QMap m_entryPositions; QString m_normTitle; QString m_hasInfoTitle; bool m_hasInfo; bool m_isBatching; bool m_markAsFuzzy; QMap > m_cache; DocPosition m_prevCachePos;//hacky hacky QVector m_jobs;//holds pointers to all the jobs for the current file }; class TextBrowser: public QTextBrowser { Q_OBJECT public: explicit TextBrowser(QWidget* parent): QTextBrowser(parent) { setContextMenuPolicy(Qt::CustomContextMenu); } void mouseDoubleClickEvent(QMouseEvent* event) override; signals: void textInsertRequested(const QString&); }; CatalogString targetAdapted(const TMEntry& entry, const CatalogString& ref); } #endif diff --git a/src/tools/widgettextcaptureconfig.cpp b/src/tools/widgettextcaptureconfig.cpp index 9545f44..87f2465 100644 --- a/src/tools/widgettextcaptureconfig.cpp +++ b/src/tools/widgettextcaptureconfig.cpp @@ -1,67 +1,68 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2012 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 "widgettextcaptureconfig.h" #include "ui_widgettextcaptureconfig.h" #include #include #include #include WidgetTextCaptureConfig::WidgetTextCaptureConfig(QWidget* parent) : QDialog(parent) , ui(new Ui_WidgetTextCapture) { setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); setWindowTitle(i18nc("@title", "Widget Text Capture")); KConfigGroup cg(KSharedConfig::openConfig(), "Development"); bool copyWidgetText = cg.readEntry("CopyWidgetText", false); QString copyWidgetTextCommand = cg.readEntry("CopyWidgetTextCommand", QString()); ui->none->setChecked(!copyWidgetText); ui->clipboard->setChecked(copyWidgetText && copyWidgetTextCommand.isEmpty()); ui->search->setChecked(copyWidgetText && !copyWidgetTextCommand.isEmpty()); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &WidgetTextCaptureConfig::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &WidgetTextCaptureConfig::reject); connect(this, &WidgetTextCaptureConfig::accepted, this, &WidgetTextCaptureConfig::writeConfig); } WidgetTextCaptureConfig::~WidgetTextCaptureConfig() { delete ui; } void WidgetTextCaptureConfig::writeConfig() { KConfig konfig(QLatin1String("kdeglobals"), KConfig::NoGlobals); KConfigGroup cg = konfig.group("Development"); cg.writeEntry("CopyWidgetText", !ui->none->isChecked()); if (ui->clipboard->isChecked()) cg.writeEntry("CopyWidgetTextCommand", QString()); else if (ui->search->isChecked()) cg.writeEntry("CopyWidgetTextCommand", "/bin/sh /usr/share/lokalize/scripts/find-gui-text.sh \"%1\" \"%2\""); konfig.sync(); } diff --git a/src/tools/widgettextcaptureconfig.h b/src/tools/widgettextcaptureconfig.h index e557d20..7c37f1f 100644 --- a/src/tools/widgettextcaptureconfig.h +++ b/src/tools/widgettextcaptureconfig.h @@ -1,43 +1,44 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2012 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 WIDGETTEXTCAPTURECONFIG_H #define WIDGETTEXTCAPTURECONFIG_H #include class Ui_WidgetTextCapture; class WidgetTextCaptureConfig: public QDialog { Q_OBJECT public: explicit WidgetTextCaptureConfig(QWidget* parent = nullptr); ~WidgetTextCaptureConfig(); public slots: void writeConfig(); private: Ui_WidgetTextCapture* ui; }; #endif // WIDGETTEXTCAPTURECONFIG_H diff --git a/src/webquery/myactioncollectionview.cpp b/src/webquery/myactioncollectionview.cpp index fc868cb..a519166 100644 --- a/src/webquery/myactioncollectionview.cpp +++ b/src/webquery/myactioncollectionview.cpp @@ -1,80 +1,81 @@ /***************************************************************************** This file is part of KAider Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "myactioncollectionview.h" #include "webquerycontroller.h" #include "project.h" #include #include #include using namespace Kross; MyActionCollectionView::MyActionCollectionView(QWidget *parent) : Kross::ActionCollectionView(parent) { setSelectionMode(QAbstractItemView::MultiSelection); //qRegisterMetaType("CatalogData"); } MyActionCollectionView::~MyActionCollectionView() { } void MyActionCollectionView::triggerSelectedActions() { foreach (const QModelIndex &index, itemSelection().indexes()) { Action* action = ActionCollectionModel::action(index); static_cast(action->object("WebQueryController"))->query(data); //we pass us into the queue. // qCWarning(LOKALIZE_LOG)<object("WebQueryController"); // Project::instance()->aaaaa()->postQuery(data, // static_cast(action->object("WebQueryController"))); // QMetaObject::invokeMethod(action->object("WebQueryController"), // SLOT(query(const CatalogData&)), // Q_ARG(CatalogData,data) // ); // connect(this,SIGNAL(query(const CatalogData&)), // action->object("WebQueryController"),SLOT(query(const CatalogData&)),Qt::QueuedConnection); // emit query(data); // disconnect(this,SIGNAL(query(const CatalogData&)), // action->object("WebQueryController"),SLOT(query(const CatalogData&))); } } diff --git a/src/webquery/myactioncollectionview.h b/src/webquery/myactioncollectionview.h index 915bab0..7135109 100644 --- a/src/webquery/myactioncollectionview.h +++ b/src/webquery/myactioncollectionview.h @@ -1,67 +1,68 @@ /***************************************************************************** This file is part of KAider Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef MYACTIONCOLLECTIONVIEW_H #define MYACTIONCOLLECTIONVIEW_H #include class Catalog; #include "webquerycontroller.h" /** @author Nick Shaforostoff */ class MyActionCollectionView : public Kross::ActionCollectionView { Q_OBJECT public: explicit MyActionCollectionView(QWidget *parent = nullptr); ~MyActionCollectionView() override; public slots: void triggerSelectedActions(); void reset() override { Kross::ActionCollectionView::reset();/*selectAll();*/ } signals: void query(const CatalogData& data); public: CatalogData data; }; #endif diff --git a/src/webquery/webquerycontroller.cpp b/src/webquery/webquerycontroller.cpp index 0876b42..abba516 100644 --- a/src/webquery/webquerycontroller.cpp +++ b/src/webquery/webquerycontroller.cpp @@ -1,137 +1,138 @@ /***************************************************************************** This file is part of KAider Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "webquerycontroller.h" #include "lokalize_debug.h" #include #include "catalog.h" #include "webqueryview.h" #include // #include #include #include WebQueryController::WebQueryController(const QString& name, QObject* parent) // : QThread(parent) : QObject(parent) , m_running(false) , m_name(name) { } void WebQueryController::query(const CatalogData& data) { m_queue.enqueue(data); if (!m_running) { m_running = true; emit doQuery(); } } QString WebQueryController::msg() { return m_queue.head().msg; } QString WebQueryController::filePath() { return QString(); } void WebQueryController::setTwinLangFilePath(QString) { } QString WebQueryController::twinLangMsg() { return QString(); } void WebQueryController::doDownloadAndFilter(QString urlStr, QString _codec, QString rx/*, int rep*/) { QString result; QUrl url; url.setUrl(urlStr); qCWarning(LOKALIZE_LOG) << "_real url: " << url.toString(); KIO::StoredTransferJob* readJob = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); connect(readJob, &KIO::StoredTransferJob::result, this, &WebQueryController::slotDownloadResult); readJob->setAutoDelete(false);//HACK HACK HACK codec = QTextCodec::codecForName(_codec.toUtf8()); filter = QRegExp(rx); } void WebQueryController::slotDownloadResult(KJob* job) { m_running = false; if (job->error()) { m_queue.dequeue(); delete job; return; } QTextStream stream(static_cast(job)->data()); stream.setCodec(codec); if (filter.indexIn(stream.readAll()) != -1) { emit postProcess(filter.cap(1)); //qCWarning(LOKALIZE_LOG)< + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef WEBQUERYCONTROLLER_H #define WEBQUERYCONTROLLER_H #include #include #include class Catalog; class WebQueryView; class QTextCodec; class KJob; struct CatalogData { QString msg; //used when the script asks for the same file but with another target language // e.g. it easier for machine to translate from russian to ukrainian than from english to ukrainian QString msg2; WebQueryView* webQueryView;//object to call slots }; /** * uses async http reading. * * currently one instance per script is used... */ class WebQueryController: public /*QThread*/QObject { Q_OBJECT public: explicit WebQueryController(const QString& name, QObject* parent); public slots: void query(const CatalogData& data); void slotDownloadResult(KJob*); signals: void addWebQueryResult(const QString&, const QString&); //These are for scripts: signals: void doQuery(); void postProcess(QString); //these are for scripts: public slots: QString msg(); QString filePath(); void setTwinLangFilePath(QString); QString twinLangMsg(); /** * Also may be used to get name of another html file (e.g. of a frame) * * @param url Lokalize escapes url before downloading * @param codec e.g. UTF-8 * @param rx RegExp that gives result in the first cap. * e.g. "
    ([^<]+)
    " */ void doDownloadAndFilter(QString url, QString codec, QString rx/*, int repeat*/); void setResult(QString); /* // If emitted calls the update() scripting function // if available. void update();*/ // protected: // void run(); private: QQueue m_queue; bool m_running; QString m_name;//of the script file //QString urlStr QTextCodec* codec; QRegExp filter; // int repeat; // QMutex m_mutex; }; #endif diff --git a/src/webquery/webqueryview.cpp b/src/webquery/webqueryview.cpp index 883efe2..7198f0a 100644 --- a/src/webquery/webqueryview.cpp +++ b/src/webquery/webqueryview.cpp @@ -1,183 +1,184 @@ /* **************************************************************************** This file is part of KAider Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "webqueryview.h" #include "lokalize_debug.h" #include "project.h" #include "catalog.h" #include "flowlayout.h" #include "ui_querycontrol.h" #include #include #include #include #include #include "webquerycontroller.h" #include #include #include #include #include #include // #include #include "myactioncollectionview.h" using namespace Kross; WebQueryView::WebQueryView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18n("Web Queries"), parent) , m_catalog(catalog) , m_splitter(new QSplitter(this)) , m_browser(new QTextBrowser(m_splitter)) , ui_queryControl(new Ui_QueryControl) , m_actions(actions) { setObjectName(QStringLiteral("WebQueryView")); setWidget(m_splitter); hide(); m_browser->viewport()->setBackgroundRole(QPalette::Background); m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QWidget* w = new QWidget(m_splitter); ui_queryControl->setupUi(w); QTimer::singleShot(0, this, &WebQueryView::initLater); } WebQueryView::~WebQueryView() { delete ui_queryControl; // delete m_flowLayout; } void WebQueryView::initLater() { connect(ui_queryControl->queryBtn, &QPushButton::clicked, ui_queryControl->actionzView, &MyActionCollectionView::triggerSelectedActions); // connect(this, &WebQueryView::addWebQueryResult, m_flowLayout, SLOT(addWebQueryResult(const QString&))); // ActionCollectionModel::Mode mode( // ActionCollectionModel::Icons // | ActionCollectionModel::ToolTips | ActionCollectionModel::UserCheckable );*/ ActionCollectionModel* m = new ActionCollectionModel(ui_queryControl->actionzView, Manager::self().actionCollection()/*, mode*/); ui_queryControl->actionzView->setModel(m); // m_boxLayout->addWidget(w); ui_queryControl->actionzView->data.webQueryView = this; m_browser->setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); QSignalMapper* signalMapper = new QSignalMapper(this); int i = m_actions.size(); while (--i >= 0) { connect(m_actions.at(i), &QAction::triggered, signalMapper, QOverload<>::of(&QSignalMapper::map)); signalMapper->setMapping(m_actions.at(i), i); } connect(signalMapper, QOverload::of(&QSignalMapper::mapped), this, &WebQueryView::slotUseSuggestion); connect(m_browser, &QTextBrowser::selectionChanged, this, &WebQueryView::slotSelectionChanged); } void WebQueryView::slotSelectionChanged() { //NOTE works fine only for dbl-click word selection //(actually, quick word insertion is exactly the purpose of this slot:) QString sel(m_browser->textCursor().selectedText()); if (!sel.isEmpty()) { emit textInsertRequested(sel); } } //TODO text may be dragged // void WebQueryView::dragEnterEvent(QDragEnterEvent* event) // { // /* if(event->mimeData()->hasUrls() && event->mimeData()->urls().first().path().endsWith(".po")) // { // //qCWarning(LOKALIZE_LOG) << " " <<; // event->acceptProposedAction(); // };*/ // } // // void WebQueryView::dropEvent(QDropEvent *event) // { // /* emit mergeOpenRequested(event->mimeData()->urls().first()); // event->acceptProposedAction();*/ // } void WebQueryView::slotNewEntryDisplayed(const DocPosition& pos) { //m_flowLayout->clearWebQueryResult(); m_browser->clear(); m_suggestions.clear(); ui_queryControl->actionzView->data.msg = m_catalog->msgid(pos); //TODO pass DocPosition also, as tmview does if (ui_queryControl->autoQuery->isChecked()) ui_queryControl->actionzView->triggerSelectedActions(); } void WebQueryView::slotUseSuggestion(int i) { if (i >= m_suggestions.size()) return; emit textInsertRequested(m_suggestions.at(i)); } void WebQueryView::addWebQueryResult(const QString& name, const QString& str) { QString html(str); html.replace('<', "<"); html.replace('>', ">"); html.append(QString("

    ")); html.prepend(QString("[%2] /%1/ ").arg(name).arg( (m_suggestions.size() < m_actions.size()) ? m_actions.at(m_suggestions.size())->shortcut().toString() : " - ")); m_browser->insertHtml(html); //m_flowLayout->addWebQueryResult(str); m_suggestions.append(str); } diff --git a/src/webquery/webqueryview.h b/src/webquery/webqueryview.h index c9e4ada..4032b49 100644 --- a/src/webquery/webqueryview.h +++ b/src/webquery/webqueryview.h @@ -1,101 +1,102 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #ifndef WEBQUERYVIEW_H #define WEBQUERYVIEW_H #include #include //#include class Catalog; class QSplitter; class QDragEnterEvent; class QDropEvent; class QEvent; class QTextBrowser; class Ui_QueryControl; #include #define WEBQUERY_SHORTCUTS 10 /** * unlike other views, we keep data like catalog pointer * in our child view: * ui_queryControl contains our own MyActionCollectionView class * that acts like dispatcher... */ class WebQueryView: public QDockWidget { Q_OBJECT public: explicit WebQueryView(QWidget*, Catalog*, const QVector&); virtual ~WebQueryView(); // void dragEnterEvent(QDragEnterEvent* event); // void dropEvent(QDropEvent*); // bool event(QEvent*); public slots: void slotNewEntryDisplayed(const DocPosition&); // void populateWebQueryActions(); // void doQuery(); void slotUseSuggestion(int i); void addWebQueryResult(const QString&, const QString&); void slotSelectionChanged(); void initLater(); signals: void textInsertRequested(const QString&); private: // QWidget* m_generalBrowser; Catalog* m_catalog; QSplitter* m_splitter; QTextBrowser* m_browser; // QHBoxLayout* m_boxLayout; // FlowLayout *m_flowLayout; Ui_QueryControl* ui_queryControl; QVector m_actions;//need them to get shortcuts QVector m_suggestions; // int m_entry; we'll use one from ui_queryControl // QString m_normTitle; // QString m_hasInfoTitle; // bool m_hasInfo; }; #endif diff --git a/src/xlifftextedit.cpp b/src/xlifftextedit.cpp index 7fe1f65..0a16395 100644 --- a/src/xlifftextedit.cpp +++ b/src/xlifftextedit.cpp @@ -1,1299 +1,1300 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff + 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "xlifftextedit.h" #include "lokalize_debug.h" #include "catalog.h" #include "cmd.h" #include "syntaxhighlighter.h" #include "prefs_lokalize.h" #include "prefs.h" #include "project.h" #include "completionstorage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline static QImage generateImage(const QString& str, const QFont& font) { // im_count++; // QTime a;a.start(); QStyleOptionButton opt; opt.fontMetrics = QFontMetrics(font); opt.text = ' ' + str + ' '; opt.rect = opt.fontMetrics.boundingRect(opt.text).adjusted(0, 0, 5, 5); opt.rect.moveTo(0, 0); QImage result(opt.rect.size(), QImage::Format_ARGB32); result.fill(0);//0xAARRGGBB QPainter painter(&result); QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, &painter); // im_time+=a.elapsed(); // qCWarning(LOKALIZE_LOG)<width() + 2 * frameWidth(); return QSize(w, h); } bool MyCompletionBox::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* e = static_cast(event); if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp) { hide(); return false; } } return KCompletionBox::eventFilter(object, event); } TranslationUnitTextEdit::~TranslationUnitTextEdit() { disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); } TranslationUnitTextEdit::TranslationUnitTextEdit(Catalog* catalog, DocPosition::Part part, QWidget* parent) : KTextEdit(parent) , m_currentUnicodeNumber(0) , m_langUsesSpaces(true) , m_catalog(catalog) , m_part(part) , m_highlighter(new SyntaxHighlighter(this)) , m_enabled(Settings::autoSpellcheck()) , m_completionBox(0) , m_cursorSelectionStart(0) , m_cursorSelectionEnd(0) { setReadOnly(part == DocPosition::Source); setUndoRedoEnabled(false); setAcceptRichText(false); m_highlighter->setActive(m_enabled); setHighlighter(m_highlighter); if (part == DocPosition::Target) { connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); connect(this, &TranslationUnitTextEdit::cursorPositionChanged, this, &TranslationUnitTextEdit::emitCursorPositionChanged); } connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &TranslationUnitTextEdit::fileLoaded); //connect (Project::instance(), &Project::configChanged, this, &TranslationUnitTextEdit::projectConfigChanged); } void TranslationUnitTextEdit::setSpellCheckingEnabled(bool enable) { Settings::setAutoSpellcheck(enable); m_enabled = enable; m_highlighter->setActive(enable); SettingsController::instance()->dirty = true; } void TranslationUnitTextEdit::setVisualizeSeparators(bool enable) { if (enable) { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() | QTextOption::ShowLineAndParagraphSeparators | QTextOption::ShowTabsAndSpaces); document()->setDefaultTextOption(textoption); } else { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() & (~QTextOption::ShowLineAndParagraphSeparators) & (~QTextOption::ShowTabsAndSpaces)); document()->setDefaultTextOption(textoption); } } void TranslationUnitTextEdit::fileLoaded() { QString langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale langLocale(langCode); // First try to use a locale name derived from the language code m_highlighter->setCurrentLanguage(langLocale.name()); // If that fails, try to use the language code directly if (m_highlighter->currentLanguage() != langLocale.name() || m_highlighter->currentLanguage().isEmpty()) { m_highlighter->setCurrentLanguage(langCode); if (m_highlighter->currentLanguage() != langCode && langCode.length() > 2) m_highlighter->setCurrentLanguage(langCode.left(2)); } //"i use an english locale while translating kde pot files from english to hebrew" Bug #181989 Qt::LayoutDirection targetLanguageDirection = Qt::LeftToRight; static QLocale::Language rtlLanguages[] = {QLocale::Arabic, QLocale::Hebrew, QLocale::Urdu, QLocale::Persian, QLocale::Pashto}; int i = sizeof(rtlLanguages) / sizeof(QLocale::Arabic); while (--i >= 0 && langLocale.language() != rtlLanguages[i]) ; if (i != -1) targetLanguageDirection = Qt::RightToLeft; setLayoutDirection(targetLanguageDirection); if (m_part == DocPosition::Source) return; //"Some language do not need space between words. For example Chinese." static QLocale::Language noSpaceLanguages[] = {QLocale::Chinese}; i = sizeof(noSpaceLanguages) / sizeof(QLocale::Chinese); while (--i >= 0 && langLocale.language() != noSpaceLanguages[i]) ; m_langUsesSpaces = (i == -1); } void TranslationUnitTextEdit::reflectApprovementState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool approved = m_catalog->isApproved(m_currentPos.entry); disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); m_highlighter->setApprovementState(approved); m_highlighter->rehighlight(); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); viewport()->setBackgroundRole(approved ? QPalette::Base : QPalette::AlternateBase); if (approved) emit approvedEntryDisplayed(); else emit nonApprovedEntryDisplayed(); bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } void TranslationUnitTextEdit::reflectUntranslatedState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } /** * makes MsgEdit reflect current entry **/ CatalogString TranslationUnitTextEdit::showPos(DocPosition docPosition, const CatalogString& refStr, bool keepCursor) { docPosition.part = m_part; m_currentPos = docPosition; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QString target = catalogString.string; _oldMsgstr = target; //_oldMsgstrAscii=document()->toPlainText(); <-- MOVED THIS TO THE END //BEGIN pos QTextCursor cursor = textCursor(); int pos = cursor.position(); int anchor = cursor.anchor(); //qCWarning(LOKALIZE_LOG)<<"called"<<"pos"<sourceWithTags(docPosition) : refStr); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); _oldMsgstrAscii = document()->toPlainText(); //BEGIN pos QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); if (pos || anchor) { //qCWarning(LOKALIZE_LOG)<<"setting"<blockSignals(true); clear(); QTextCursor c = textCursor(); insertContent(c, catStr, refStr); document()->blockSignals(false); if (m_part == DocPosition::Target) m_highlighter->setSourceString(refStr.string); else //reflectApprovementState() does this for Target m_highlighter->rehighlight(); //explicitly because the signals were disabled } #if 0 struct SearchFunctor { virtual int operator()(const QString& str, int startingPos); }; int SearchFunctor::operator()(const QString& str, int startingPos) { return str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); } struct AlternativeSearchFunctor: public SearchFunctor { int operator()(const QString& str, int startingPos); }; int AlternativeSearchFunctor::operator()(const QString& str, int startingPos) { int tagPos = str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); int diffStartPos = str.indexOf("{KBABEL", startingPos); int diffEndPos = str.indexOf("{/KBABEL", startingPos); int diffPos = qMin(diffStartPos, diffEndPos); if (diffPos == -1) diffPos = qMax(diffStartPos, diffEndPos); int result = qMin(tagPos, diffPos); if (result == -1) result = qMax(tagPos, diffPos); return result; } #endif void insertContent(QTextCursor& cursor, const CatalogString& catStr, const CatalogString& refStr, bool insertText) { //settings for TMView QTextCharFormat chF = cursor.charFormat(); QFont font = cursor.document()->defaultFont(); //font.setWeight(chF.fontWeight()); QMap posToTag; int i = catStr.tags.size(); while (--i >= 0) { //qCDebug(LOKALIZE_LOG)<<"\t"< sourceTagIdToIndex = refStr.tagIdToIndex(); int refTagIndexOffset = sourceTagIdToIndex.size(); i = 0; int prev = 0; while ((i = catStr.string.indexOf(TAGRANGE_IMAGE_SYMBOL, i)) != -1) { #if 0 SearchFunctor nextStopSymbol = AlternativeSearchFunctor(); char state = '0'; while ((i = nextStopSymbol(catStr.string, i)) != -1) { //handle diff display for TMView if (catStr.string.at(i) != TAGRANGE_IMAGE_SYMBOL) { if (catStr.string.at(i + 1) == '/') state = '0'; else if (catStr.string.at(i + 8) == 'D') state = '-'; else state = '+'; continue; } #endif if (insertText) cursor.insertText(catStr.string.mid(prev, i - prev)); else { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, i - prev); cursor.deleteChar();//delete TAGRANGE_IMAGE_SYMBOL to insert it properly } if (!posToTag.contains(i)) { prev = ++i; continue; } int tagIndex = posToTag.value(i); InlineTag tag = catStr.tags.at(tagIndex); QString name = tag.id; QString text; if (tag.type == InlineTag::mrk) text = QStringLiteral("*"); else if (!tag.equivText.isEmpty()) text = tag.equivText; //TODO add number? when? -- right now this is done for gettext qt's 156 mark else text = QString::number(sourceTagIdToIndex.contains(tag.id) ? sourceTagIdToIndex.value(tag.id) : (tagIndex + refTagIndexOffset)); if (tag.start != tag.end) { //qCWarning(LOKALIZE_LOG)<<"b"<resource(QTextDocument::ImageResource, QUrl(name)).isNull()) cursor.document()->addResource(QTextDocument::ImageResource, QUrl(name), generateImage(text, font)); cursor.insertImage(name);//NOTE what if twice the same name? cursor.setCharFormat(chF); prev = ++i; } cursor.insertText(catStr.string.mid(prev)); } void TranslationUnitTextEdit::contentsChanged(int offset, int charsRemoved, int charsAdded) { Q_ASSERT(m_catalog->targetLangCode().length()); Q_ASSERT(Project::instance()->targetLangCode().length()); //qCWarning(LOKALIZE_LOG)<<"contentsChanged. offset"<toPlainText(); if (editTextAscii == _oldMsgstrAscii) { //qCWarning(LOKALIZE_LOG)<<"stopping"<targetWithTags(pos).string; const QStringRef addedText = editText.midRef(offset, charsAdded); //BEGIN XLIFF markup handling //protect from tag removal //TODO use midRef when Qt 4.8 is in distros bool markupRemoved = charsRemoved && target.midRef(offset, charsRemoved).contains(TAGRANGE_IMAGE_SYMBOL); bool markupAdded = charsAdded && addedText.contains(TAGRANGE_IMAGE_SYMBOL); if (markupRemoved || markupAdded) { bool modified = false; CatalogString targetWithTags = m_catalog->targetWithTags(m_currentPos); //special case when the user presses Del w/o selection if (!charsAdded && charsRemoved == 1) { int i = targetWithTags.tags.size(); while (--i >= 0) { if (targetWithTags.tags.at(i).start == offset || targetWithTags.tags.at(i).end == offset) { modified = true; pos.offset = targetWithTags.tags.at(i).start; m_catalog->push(new DelTagCmd(m_catalog, pos)); } } } else if (!markupAdded) { //check if all { plus } tags were selected modified = removeTargetSubstring(offset, charsRemoved, /*refresh*/false); if (modified && charsAdded) m_catalog->push(new InsTextCmd(m_catalog, pos, addedText.toString())); } //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true); if (!modified) { //qCWarning(LOKALIZE_LOG)<<"stop"; return; } } //END XLIFF markup handling else { if (charsRemoved) m_catalog->push(new DelTextCmd(m_catalog, pos, _oldMsgstr.mid(offset, charsRemoved))); _oldMsgstr = editText; //newStr becomes OldStr _oldMsgstrAscii = editTextAscii; //qCWarning(LOKALIZE_LOG)<<"char"<push(new InsTextCmd(m_catalog, pos, addedText.toString())); } /* TODO if (_leds) { if (m_catalog->msgstr(pos).isEmpty()) _leds->ledUntr->on(); else _leds->ledUntr->off(); } */ requestToggleApprovement(); reflectUntranslatedState(); // for mergecatalog (remove entry from index) // and for statusbar emit contentsModified(m_currentPos); if (charsAdded == 1) { int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, offset - 1); int len = (offset - sp); int wordCompletionLength = Settings::self()->wordCompletionLength(); if (wordCompletionLength >= 3 && len >= wordCompletionLength) doCompletion(offset + 1); else if (m_completionBox) m_completionBox->hide(); } else if (m_completionBox) m_completionBox->hide(); //qCWarning(LOKALIZE_LOG)<<"finish"; } bool TranslationUnitTextEdit::removeTargetSubstring(int delStart, int delLen, bool refresh) { if (Q_UNLIKELY(m_currentPos.entry == -1)) return false; if (!::removeTargetSubstring(m_catalog, m_currentPos, delStart, delLen)) return false; requestToggleApprovement(); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); } emit contentsModified(m_currentPos.entry); return true; } void TranslationUnitTextEdit::insertCatalogString(CatalogString catStr, int start, bool refresh) { QString REMOVEME = QStringLiteral("REMOVEME"); CatalogString sourceForReferencing = m_catalog->sourceWithTags(m_currentPos); const CatalogString target = m_catalog->targetWithTags(m_currentPos); QHash id2tagIndex; int i = sourceForReferencing.tags.size(); while (--i >= 0) id2tagIndex.insert(sourceForReferencing.tags.at(i).id, i); //remove markup that is already in target, to avoid duplicates if the string being inserted contains it as well foreach (const InlineTag& tag, target.tags) { if (id2tagIndex.contains(tag.id)) sourceForReferencing.tags[id2tagIndex.value(tag.id)].id = REMOVEME; } //iterating from the end is essential i = sourceForReferencing.tags.size(); while (--i >= 0) if (sourceForReferencing.tags.at(i).id == REMOVEME) sourceForReferencing.tags.removeAt(i); adaptCatalogString(catStr, sourceForReferencing); ::insertCatalogString(m_catalog, m_currentPos, catStr, start); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, catStr.string.size()); setTextCursor(cursor); } } const QString LOKALIZE_XLIFF_MIMETYPE = QStringLiteral("application/x-lokalize-xliff+xml"); QMimeData* TranslationUnitTextEdit::createMimeDataFromSelection() const { QMimeData *mimeData = new QMimeData; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); QMap tagPlaces; if (fillTagPlaces(tagPlaces, catalogString, start, end - start)) { //transform CatalogString //TODO substring method catalogString.string = catalogString.string.mid(start, end - start); QList::iterator it = catalogString.tags.begin(); while (it != catalogString.tags.end()) { if (!tagPlaces.contains(it->start)) it = catalogString.tags.erase(it); else { it->start -= start; it->end -= start; ++it; } } QByteArray a; QDataStream out(&a, QIODevice::WriteOnly); QVariant v; qVariantSetValue(v, catalogString); out << v; mimeData->setData(LOKALIZE_XLIFF_MIMETYPE, a); } QString text = catalogString.string; text.remove(TAGRANGE_IMAGE_SYMBOL); mimeData->setText(text); return mimeData; } void TranslationUnitTextEdit::dragEnterEvent(QDragEnterEvent * event) { QObject* dragSource = event->source(); if (dragSource->objectName().compare("qt_scrollarea_viewport") == 0) dragSource = dragSource->parent(); //This is a deplacement within the Target area if (m_part == DocPosition::Target && this == dragSource) { QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); m_cursorSelectionEnd = end; m_cursorSelectionStart = start; } QTextEdit::dragEnterEvent(event); } void TranslationUnitTextEdit::dropEvent(QDropEvent * event) { //Ensure the cursor moves to the correct location if (m_part == DocPosition::Target) { setTextCursor(cursorForPosition(event->pos())); //This is a copy modifier, disable the selection flags if (event->keyboardModifiers() & Qt::ControlModifier) { m_cursorSelectionEnd = 0; m_cursorSelectionStart = 0; } } QTextEdit::dropEvent(event); } void TranslationUnitTextEdit::insertFromMimeData(const QMimeData * source) { if (m_part == DocPosition::Source) return; if (source->hasFormat(LOKALIZE_XLIFF_MIMETYPE)) { //qCWarning(LOKALIZE_LOG)<<"has"; QVariant v; QByteArray data = source->data(LOKALIZE_XLIFF_MIMETYPE); QDataStream in(&data, QIODevice::ReadOnly); in >> v; //qCWarning(LOKALIZE_LOG)<<"ins"<(v).string<(v).ranges.size(); int start = 0; m_catalog->beginMacro(i18nc("@item Undo action item", "Insert text with markup")); QTextCursor cursor = textCursor(); if (cursor.hasSelection()) { start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); removeTargetSubstring(start, end - start); cursor.setPosition(start); setTextCursor(cursor); } else //sets right cursor position implicitly -- needed for mouse paste { QMimeData mimeData; mimeData.setText(QString()); if (m_cursorSelectionEnd != m_cursorSelectionStart) { int oldCursorPosition = textCursor().position(); removeTargetSubstring(m_cursorSelectionStart, m_cursorSelectionEnd - m_cursorSelectionStart); if (oldCursorPosition >= m_cursorSelectionEnd) { cursor.setPosition(oldCursorPosition - (m_cursorSelectionEnd - m_cursorSelectionStart)); setTextCursor(cursor); } } KTextEdit::insertFromMimeData(&mimeData); start = textCursor().position(); } insertCatalogString(v.value(), start); m_catalog->endMacro(); } else { QString text = source->text(); text.remove(TAGRANGE_IMAGE_SYMBOL); insertPlainText(text); } } static bool isMasked(const QString & str, uint col) { if (col == 0 || str.isEmpty()) return false; uint counter = 0; int pos = col; while (pos >= 0 && str.at(pos) == '\\') { counter++; pos--; } return !(bool)(counter % 2); } void TranslationUnitTextEdit::keyPressEvent(QKeyEvent * keyEvent) { QString spclChars = QStringLiteral("abfnrtv'?\\"); if (keyEvent->matches(QKeySequence::MoveToPreviousPage)) emit gotoPrevRequested(); else if (keyEvent->matches(QKeySequence::MoveToNextPage)) emit gotoNextRequested(); else if (keyEvent->matches(QKeySequence::Undo)) emit undoRequested(); else if (keyEvent->matches(QKeySequence::Redo)) emit redoRequested(); else if (keyEvent->matches(QKeySequence::Find)) emit findRequested(); else if (keyEvent->matches(QKeySequence::FindNext)) emit findNextRequested(); else if (keyEvent->matches(QKeySequence::Replace)) emit replaceRequested(); else if (keyEvent->modifiers() == (Qt::AltModifier | Qt::ControlModifier)) { if (keyEvent->key() == Qt::Key_Home) emit gotoFirstRequested(); else if (keyEvent->key() == Qt::Key_End) emit gotoLastRequested(); } else if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::MoveToPreviousLine)) { //static QTime lastUpDownPress; //if (lastUpDownPress.msecsTo(QTime::currentTime())<500) { keyEvent->setAccepted(true); bool up = keyEvent->key() == Qt::Key_Up; QTextCursor c = textCursor(); if (!c.movePosition(up ? QTextCursor::Up : QTextCursor::Down)) { QTextCursor::MoveOperation op; if (up && !c.atStart()) op = QTextCursor::Start; else if (!up && !c.atEnd()) op = QTextCursor::End; else if (up) { emit gotoPrevRequested(); op = QTextCursor::End; } else { emit gotoNextRequested(); op = QTextCursor::Start; } c.movePosition(op); } setTextCursor(c); } //lastUpDownPress=QTime::currentTime(); } else if (m_part == DocPosition::Source) return KTextEdit::keyPressEvent(keyEvent); //BEGIN GENERAL // ALT+123 feature TODO this is general so should be on another level else if ((keyEvent->modifiers()&Qt::AltModifier) && !keyEvent->text().isEmpty() && keyEvent->text().at(0).isDigit()) { QString text = keyEvent->text(); while (!text.isEmpty() && text.at(0).isDigit()) { m_currentUnicodeNumber = 10 * m_currentUnicodeNumber + (text.at(0).digitValue()); text.remove(0, 1); } KTextEdit::keyPressEvent(keyEvent); } //END GENERAL else if (!keyEvent->modifiers() && (keyEvent->key() == Qt::Key_Backspace || keyEvent->key() == Qt::Key_Delete)) { //only for cases when: //-BkSpace was hit and cursor was atStart //-Del was hit and cursor was atEnd if (Q_UNLIKELY(!m_catalog->isApproved(m_currentPos.entry) && !textCursor().hasSelection()) && ((textCursor().atStart() && keyEvent->key() == Qt::Key_Backspace) || (textCursor().atEnd() && keyEvent->key() == Qt::Key_Delete))) requestToggleApprovement(); else KTextEdit::keyPressEvent(keyEvent); } else if (keyEvent->key() == Qt::Key_Space && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainText(QChar(0x00a0U)); else if (keyEvent->key() == Qt::Key_Minus && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainText(QChar(0x0000AD)); //BEGIN clever editing else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { if (m_completionBox && m_completionBox->isVisible()) { if (m_completionBox->currentItem()) completionActivated(m_completionBox->currentItem()->text()); else qCWarning(LOKALIZE_LOG) << "avoided a crash. a case for bug 238835!"; m_completionBox->hide(); return; } if (m_catalog->type() != Gettext) return KTextEdit::keyPressEvent(keyEvent); QString str = toPlainText(); QTextCursor t = textCursor(); int pos = t.position(); QString ins; if (keyEvent->modifiers()&Qt::ShiftModifier) { if (pos > 0 && !str.isEmpty() && str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) { ins = 'n'; } else { ins = QStringLiteral("\\n"); } } else if (!(keyEvent->modifiers()&Qt::ControlModifier)) { if (m_langUsesSpaces && pos > 0 && !str.isEmpty() && !str.at(pos - 1).isSpace()) { if (str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) ins = QLatin1Char('\\'); // if there is no new line at the end if (pos < 2 || str.midRef(pos - 2, 2) != QLatin1String("\\n")) ins += QLatin1Char(' '); } else if (str.isEmpty()) { ins = QStringLiteral("\\n"); } } if (!str.isEmpty()) { ins += '\n'; insertPlainText(ins); } else KTextEdit::keyPressEvent(keyEvent); } else if (m_catalog->type() != Gettext) KTextEdit::keyPressEvent(keyEvent); else if ((keyEvent->modifiers()&Qt::ControlModifier) ? (keyEvent->key() == Qt::Key_D) : (keyEvent->key() == Qt::Key_Delete) && textCursor().atEnd()) { qCWarning(LOKALIZE_LOG) << "workaround for Qt/X11 bug"; QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); //workaround for Qt/X11 bug: if Del on NumPad is pressed, then pos is beyond end if (pos == str.size()) --pos; if (!str.isEmpty() && str.at(pos) == '\\' && !isMasked(str, pos) && pos < str.length() - 1 && spclChars.contains(str.at(pos + 1))) { t.deleteChar(); } } t.deleteChar(); setTextCursor(t); } else if ((!keyEvent->modifiers() && keyEvent->key() == Qt::Key_Backspace) || ((keyEvent->modifiers() & Qt::ControlModifier) && keyEvent->key() == Qt::Key_H)) { QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); if (!str.isEmpty() && pos > 0 && spclChars.contains(str.at(pos - 1))) { if (pos > 1 && str.at(pos - 2) == QLatin1Char('\\') && !isMasked(str, pos - 2)) { t.deletePreviousChar(); t.deletePreviousChar(); setTextCursor(t); //qCWarning(LOKALIZE_LOG)<<"set-->"<key() == Qt::Key_Tab) insertPlainText(QStringLiteral("\\t")); else KTextEdit::keyPressEvent(keyEvent); //END clever editing } void TranslationUnitTextEdit::keyReleaseEvent(QKeyEvent * e) { if ((e->key() == Qt::Key_Alt) && m_currentUnicodeNumber >= 32) { insertPlainText(QChar(m_currentUnicodeNumber)); m_currentUnicodeNumber = 0; } else KTextEdit::keyReleaseEvent(e); } QString TranslationUnitTextEdit::toPlainText() { QTextCursor cursor = textCursor(); cursor.select(QTextCursor::Document); QString text = cursor.selectedText(); text.replace(QChar(8233), '\n'); /* int ii=text.size(); while(--ii>=0) qCWarning(LOKALIZE_LOG)<push(new InsTagCmd(m_catalog, currentPos(), tag)); showPos(currentPos(), CatalogString(),/*keepCursor*/true); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, tag.end + 1 + tag.isPaired()); setFocus(); } int TranslationUnitTextEdit::strForMicePosIfUnderTag(QPoint mice, CatalogString & str, bool tryHarder) { if (m_currentPos.entry == -1) return -1; QTextCursor cursor = cursorForPosition(mice); int pos = cursor.position(); str = m_catalog->catalogString(m_currentPos); if (pos == -1 || pos >= str.string.size()) return -1; //qCWarning(LOKALIZE_LOG)<<"here1"<0) // { // cursor.movePosition(QTextCursor::Left); // mice.setX(mice.x()+cursorRect(cursor).width()/2); // pos=cursorForPosition(mice).position(); // } if (str.string.at(pos) != TAGRANGE_IMAGE_SYMBOL) { bool cont = tryHarder && --pos >= 0 && str.string.at(pos) == TAGRANGE_IMAGE_SYMBOL; if (!cont) return -1; } int result = str.tags.size(); while (--result >= 0 && str.tags.at(result).start != pos && str.tags.at(result).end != pos) ; return result; } void TranslationUnitTextEdit::mouseReleaseEvent(QMouseEvent * event) { if (event->button() == Qt::LeftButton) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1 && m_part == DocPosition::Source) { emit tagInsertRequested(str.tags.at(pos)); event->accept(); return; } } KTextEdit::mouseReleaseEvent(event); } void TranslationUnitTextEdit::contextMenuEvent(QContextMenuEvent * event) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1) { QString xid = str.tags.at(pos).xid; if (!xid.isEmpty()) { QMenu menu; int entry = m_catalog->unitById(xid); /* QAction* findUnit=menu.addAction(entry>=m_catalog->numberOfEntries()? i18nc("@action:inmenu","Show the binary unit"): i18nc("@action:inmenu","Go to the referenced entry")); */ QAction* result = menu.exec(event->globalPos()); if (result) { if (entry >= m_catalog->numberOfEntries()) emit binaryUnitSelectRequested(xid); else emit gotoEntryRequested(DocPosition(entry)); event->accept(); } return; } } if (textCursor().hasSelection()) { QMenu menu; menu.addAction(i18nc("@action:inmenu", "Lookup selected text in translation memory")); if (menu.exec(event->globalPos())) emit tmLookupRequested(m_part, textCursor().selectedText()); return; } if (m_part != DocPosition::Target) return; KTextEdit::contextMenuEvent(event); #if 0 QTextCursor wordSelectCursor = cursorForPosition(event->pos()); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) { QMenu menu; QMenu suggestions; foreach (const QString& s, m_highlighter->suggestionsForWord(wordSelectCursor.selectedText())) suggestions.addAction(s); if (!suggestions.isEmpty()) { QAction* answer = suggestions.exec(event->globalPos()); if (answer) { m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(answer->text()); m_catalog->endMacro(); } } } #endif // QMenu menu; // QAction* spellchecking=menu.addAction(); // event->accept(); } void TranslationUnitTextEdit::wheelEvent(QWheelEvent * event) { //Override default KTextEdit behavior which ignores Ctrl+wheelEvent when the field is not ReadOnly (i/o zooming) if (m_part == DocPosition::Target && !Settings::mouseWheelGo() && (event->modifiers() == Qt::ControlModifier)) { float delta = event->angleDelta().y() / 120.f; zoomInF(delta); return; } if (m_part == DocPosition::Source || !Settings::mouseWheelGo()) return KTextEdit::wheelEvent(event); switch (event->modifiers()) { case Qt::ControlModifier: if (event->delta() > 0) emit gotoPrevFuzzyRequested(); else emit gotoNextFuzzyRequested(); break; case Qt::AltModifier: if (event->delta() > 0) emit gotoPrevUntranslatedRequested(); else emit gotoNextUntranslatedRequested(); break; case Qt::ControlModifier + Qt::ShiftModifier: if (event->delta() > 0) emit gotoPrevFuzzyUntrRequested(); else emit gotoNextFuzzyUntrRequested(); break; case Qt::ShiftModifier: return KTextEdit::wheelEvent(event); default: if (event->delta() > 0) emit gotoPrevRequested(); else emit gotoNextRequested(); } } void TranslationUnitTextEdit::spellReplace() { QTextCursor wordSelectCursor = textCursor(); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (!m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) return; const QStringList& suggestions = m_highlighter->suggestionsForWord(wordSelectCursor.selectedText()); if (suggestions.isEmpty()) return; m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(suggestions.first()); m_catalog->endMacro(); } bool TranslationUnitTextEdit::event(QEvent * event) { #ifdef Q_OS_MAC if (event->type() == QEvent::InputMethod) { QInputMethodEvent* e = static_cast(event); insertPlainText(e->commitString()); e->accept(); return true; } #endif if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); CatalogString str; int pos = strForMicePosIfUnderTag(helpEvent->pos(), str, true); if (pos != -1) { QString tooltip = str.tags.at(pos).displayName(); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } QString tip; QString langCode = m_highlighter->currentLanguage(); bool nospell = langCode.isEmpty(); if (nospell) langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale l(langCode); if (l.language() != QLocale::C) tip = l.nativeLanguageName() + QLatin1String(" ("); tip += langCode; if (l.language() != QLocale::C) tip += ')'; if (nospell) tip += QLatin1String(" - ") % i18n("no spellcheck available"); QToolTip::showText(helpEvent->globalPos(), tip); } return KTextEdit::event(event); } void TranslationUnitTextEdit::tagMenu() { doTag(false); } void TranslationUnitTextEdit::tagImmediate() { doTag(true); } void TranslationUnitTextEdit::doTag(bool immediate) { QMenu menu; QAction* txt = 0; CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); int count = sourceWithTags.tags.size(); if (count) { QMap tagIdToIndex = m_catalog->targetWithTags(m_currentPos).tagIdToIndex(); bool hasActive = false; for (int i = 0; i < count; ++i) { //txt=menu.addAction(sourceWithTags.ranges.at(i)); txt = menu.addAction(QString::number(i)/*+" "+sourceWithTags.ranges.at(i).id*/); txt->setData(QVariant(i)); if (!hasActive && !tagIdToIndex.contains(sourceWithTags.tags.at(i).id)) { if (immediate) { insertTag(sourceWithTags.tags.at(txt->data().toInt())); return; } hasActive = true; menu.setActiveAction(txt); } } if (immediate) return; txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (!txt) return; insertTag(sourceWithTags.tags.at(txt->data().toInt())); } else { if (Q_UNLIKELY(Project::instance()->markup().isEmpty())) return; //QRegExp tag("(<[^>]*>)+|\\&\\w+\\;"); QRegExp tag(Project::instance()->markup()); tag.setMinimal(true); QString en = m_catalog->sourceWithTags(m_currentPos).string; QString target(toPlainText()); en.remove('\n'); target.remove('\n'); int pos = 0; //tag.indexIn(en); int posInMsgStr = 0; while ((pos = tag.indexIn(en, pos)) != -1) { /* QString str(tag.cap(0)); str.replace("&","&&");*/ txt = menu.addAction(tag.cap(0)); pos += tag.matchedLength(); if (posInMsgStr != -1 && (posInMsgStr = target.indexOf(tag.cap(0), posInMsgStr)) == -1) { if (immediate) { insertPlainText(txt->text()); return; } menu.setActiveAction(txt); } else if (posInMsgStr != -1) posInMsgStr += tag.matchedLength(); } if (!txt || immediate) return; //txt=menu.exec(_msgidEdit->mapToGlobal(QPoint(0,0))); txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (txt) insertPlainText(txt->text()); } } void TranslationUnitTextEdit::source2target() { CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); QString text = sourceWithTags.string; QString out; QString ctxt = m_catalog->context(m_currentPos.entry).first(); QRegExp delimiter(QStringLiteral("\\s*,\\s*")); //TODO ask for the fillment if the first time. //BEGIN KDE specific part if (ctxt.startsWith(QLatin1String("NAME OF TRANSLATORS")) || text.startsWith(QLatin1String("_: NAME OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorLocalizedName())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorLocalizedName(); } } else if (ctxt.startsWith(QLatin1String("EMAIL OF TRANSLATORS")) || text.startsWith(QLatin1String("_: EMAIL OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorEmail())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorEmail(); } } else if (/*_catalog->isGeneratedFromDocbook() &&*/ text.startsWith(QLatin1String("ROLES_OF_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("\n" "\n" "
    ") % Settings::authorEmail() % QLatin1String("
    \n" "
    "); } else if (text.startsWith(QLatin1String("CREDIT_FOR_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("") % Settings::authorLocalizedName() % '\n' % QLatin1String("") % Settings::authorEmail() % QLatin1String(""); } //END KDE specific part else { m_catalog->beginMacro(i18nc("@item Undo action item", "Copy source to target")); removeTargetSubstring(0, -1,/*refresh*/false); insertCatalogString(sourceWithTags, 0,/*refresh*/false); m_catalog->endMacro(); showPos(m_currentPos, sourceWithTags,/*keepCursor*/false); requestToggleApprovement(); } if (!out.isEmpty()) { QTextCursor t = textCursor(); t.movePosition(QTextCursor::End); t.insertText(out); setTextCursor(t); } } void TranslationUnitTextEdit::requestToggleApprovement() { if (m_catalog->isApproved(m_currentPos.entry) || !Settings::autoApprove()) return; bool skip = m_catalog->isPlural(m_currentPos); if (skip) { skip = false; DocPos pos(m_currentPos); for (pos.form = 0; pos.form < m_catalog->numberOfPluralForms(); ++(pos.form)) skip = skip || !m_catalog->isModified(pos); } if (!skip) emit toggleApprovementRequested(); } void TranslationUnitTextEdit::cursorToStart() { QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); setTextCursor(t); } void TranslationUnitTextEdit::doCompletion(int pos) { QTime a; a.start(); QString target = m_catalog->targetWithTags(m_currentPos).string; int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, pos - 1); int len = (pos - sp) - 1; QStringList s = CompletionStorage::instance()->makeCompletion(QString::fromRawData(target.unicode() + sp + 1, len)); if (!m_completionBox) { //BEGIN creation m_completionBox = new MyCompletionBox(this); connect(m_completionBox, &MyCompletionBox::activated, this, &TranslationUnitTextEdit::completionActivated); m_completionBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); //END creation } m_completionBox->setItems(s); if (s.size() && !s.first().isEmpty()) { m_completionBox->setCurrentRow(0); //qApp->removeEventFilter( m_completionBox ); if (!m_completionBox->isVisible()) //NOTE remove the check if kdelibs gets adapted m_completionBox->show(); m_completionBox->resize(m_completionBox->sizeHint()); QPoint p = cursorRect().bottomRight(); if (p.x() < 10) //workaround Qt bug p.rx() += textCursor().verticalMovementX() + QFontMetrics(currentFont()).width('W'); m_completionBox->move(viewport()->mapToGlobal(p)); } else m_completionBox->hide(); } void TranslationUnitTextEdit::doExplicitCompletion() { doCompletion(textCursor().anchor()); } void TranslationUnitTextEdit::completionActivated(const QString & semiWord) { QTextCursor cursor = textCursor(); cursor.insertText(semiWord); setTextCursor(cursor); } diff --git a/src/xlifftextedit.h b/src/xlifftextedit.h index de415fd..0f8fe9c 100644 --- a/src/xlifftextedit.h +++ b/src/xlifftextedit.h @@ -1,172 +1,173 @@ /* **************************************************************************** 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 . **************************************************************************** */ #ifndef XLIFFTEXTEDITOR_H #define XLIFFTEXTEDITOR_H #include "pos.h" #include "catalogstring.h" #include class QMouseEvent; class SyntaxHighlighter;//TODO rename class KCompletionBox; class MyCompletionBox; class TranslationUnitTextEdit: public KTextEdit { Q_OBJECT public: explicit TranslationUnitTextEdit(Catalog* catalog, DocPosition::Part part, QWidget* parent = nullptr); ~TranslationUnitTextEdit() override; //NOTE remove this when Qt is fixed (hack for unbreakable spaces bug #162016) QString toPlainText(); ///@returns targetWithTags for the sake of not calling XliffStorage/doContent twice CatalogString showPos(DocPosition pos, const CatalogString& refStr = CatalogString(), bool keepCursor = true); DocPosition currentPos()const { return m_currentPos; } void cursorToStart(); bool isSpellCheckingEnabled() const { return m_enabled; } void setSpellCheckingEnabled(bool enable); void setVisualizeSeparators(bool enable); bool shouldBlockBeSpellChecked(const QString &block) const override { Q_UNUSED(block); return true; } public slots: void reflectApprovementState(); void reflectUntranslatedState(); bool removeTargetSubstring(int start = 0, int end = -1, bool refresh = true); void insertCatalogString(CatalogString catStr, int start = 0, bool refresh = true); void source2target(); void tagMenu(); void tagImmediate(); void insertTag(InlineTag tag); void spellReplace(); void emitCursorPositionChanged();//for leds void doExplicitCompletion(); protected: void keyPressEvent(QKeyEvent *keyEvent) override; void keyReleaseEvent(QKeyEvent* e) override; QMimeData* createMimeDataFromSelection() const override; void insertFromMimeData(const QMimeData* source) override; void mouseReleaseEvent(QMouseEvent* event) override; void dropEvent(QDropEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override; void wheelEvent(QWheelEvent *event) override; bool event(QEvent *event) override; private: ///@a refStr is for proper numbering void setContent(const CatalogString& catStr, const CatalogString& refStr = CatalogString()); int strForMicePosIfUnderTag(QPoint mice, CatalogString& str, bool tryHarder = false); void requestToggleApprovement(); void doTag(bool immediate); void doCompletion(int pos); private slots: //for Undo/Redo tracking void contentsChanged(int position, int charsRemoved, int charsAdded); void completionActivated(const QString&); void fileLoaded(); signals: void toggleApprovementRequested(); void undoRequested(); void redoRequested(); void findRequested(); void findNextRequested(); void replaceRequested(); void gotoFirstRequested(); void gotoLastRequested(); void gotoPrevRequested(); void gotoNextRequested(); void gotoPrevFuzzyRequested(); void gotoNextFuzzyRequested(); void gotoPrevUntranslatedRequested(); void gotoNextUntranslatedRequested(); void gotoPrevFuzzyUntrRequested(); void gotoNextFuzzyUntrRequested(); void gotoEntryRequested(const DocPosition&); void tagInsertRequested(const InlineTag& tag); void binaryUnitSelectRequested(const QString&); void tmLookupRequested(DocPosition::Part, const QString&); void contentsModified(const DocPosition&); void approvedEntryDisplayed(); void nonApprovedEntryDisplayed(); void translatedEntryDisplayed(); void untranslatedEntryDisplayed(); void cursorPositionChanged(int column); private: int m_currentUnicodeNumber; //alt+NUM thing bool m_langUsesSpaces; //e.g. Chinese doesn't Catalog* m_catalog; DocPosition::Part m_part; DocPosition m_currentPos; SyntaxHighlighter* m_highlighter; bool m_enabled; MyCompletionBox* m_completionBox; //for undo/redo tracking QString _oldMsgstr; QString _oldMsgstrAscii; //HACK to workaround #218246 //For text move with mouse int m_cursorSelectionStart; int m_cursorSelectionEnd; }; void insertContent(QTextCursor& cursor, const CatalogString& catStr, const CatalogString& refStr = CatalogString(), bool insertText = true); #endif