diff --git a/src/filesearch/filesearchtab.cpp b/src/filesearch/filesearchtab.cpp index acb12f1..11058a6 100644 --- a/src/filesearch/filesearchtab.cpp +++ b/src/filesearch/filesearchtab.cpp @@ -1,922 +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[] = {QGuiApplication::primaryScreen()->availableGeometry().width() / 3, QGuiApplication::primaryScreen()->availableGeometry().width() / 3}; int column = sizeof(maxInitialWidths) / sizeof(int); while (--column >= 0) view->setColumnWidth(column, maxInitialWidths[column]); //END resizeColumnToContents int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); setXMLFile(QStringLiteral("filesearchtabui.rc"), true); dbusObjectPath(); KActionCollection* ac = actionCollection(); KActionCategory* srf = new KActionCategory(i18nc("@title actions category", "Search and replace in files"), ac); m_searchFileListView = new SearchFileListView(this); //m_searchFileListView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_searchFileListView); srf->addAction(QStringLiteral("showfilelist_action"), m_searchFileListView->toggleViewAction()); connect(m_searchFileListView, &SearchFileListView::fileOpenRequested, this, QOverload::of(&FileSearchTab::fileOpenRequested)); m_massReplaceView = new MassReplaceView(this); addDockWidget(Qt::RightDockWidgetArea, m_massReplaceView); srf->addAction(QStringLiteral("showmassreplace_action"), m_massReplaceView->toggleViewAction()); connect(m_massReplaceView, &MassReplaceView::previewRequested, m_model, &FileSearchModel::setReplacePreview); connect(m_massReplaceView, &MassReplaceView::replaceRequested, this, &FileSearchTab::massReplace); //m_massReplaceView->hide(); m_qaView = new QaView(this); m_qaView->hide(); addDockWidget(Qt::RightDockWidgetArea, m_qaView); srf->addAction(QStringLiteral("showqa_action"), m_qaView->toggleViewAction()); connect(m_qaView, &QaView::rulesChanged, this, &FileSearchTab::performSearch); connect(m_qaView->toggleViewAction(), &QAction::toggled, this, &FileSearchTab::performSearch, Qt::QueuedConnection); view->header()->restoreState(readUiState("FileSearchResultsHeaderState")); } FileSearchTab::~FileSearchTab() { stopSearch(); writeUiState("FileSearchResultsHeaderState", ui_fileSearchOptions->treeView->header()->saveState()); ids.removeAll(m_dbusId); } void FileSearchTab::performSearch() { if (m_searchFileListView->files().isEmpty()) { addFilesToSearch(doScanRecursive(QDir(Project::instance()->poDir()))); if (m_searchFileListView->files().isEmpty()) return; } m_model->clear(); statusBarItems.insert(1, QString()); m_searchFileListView->scrollTo(); m_lastSearchNumber++; SearchParams sp; sp.sourcePattern.setPattern(ui_fileSearchOptions->querySource->text()); sp.targetPattern.setPattern(ui_fileSearchOptions->queryTarget->text()); sp.invertSource = ui_fileSearchOptions->invertSource->isChecked(); sp.invertTarget = ui_fileSearchOptions->invertTarget->isChecked(); QVector rules = m_qaView->isVisible() ? m_qaView->rules() : QVector(); if (sp.isEmpty() && rules.isEmpty()) return; if (!ui_fileSearchOptions->regEx->isChecked()) { sp.sourcePattern.setPatternSyntax(QRegExp::FixedString); sp.targetPattern.setPatternSyntax(QRegExp::FixedString); } /* else { sp.sourcePattern.setMinimal(true); sp.targetPattern.setMinimal(true); } */ stopSearch(); m_massReplaceView->deactivatePreview(); QStringList files = m_searchFileListView->files(); for (int i = 0; i < files.size(); i += 100) { QStringList batch; int lim = qMin(files.size(), i + 100); for (int j = i; j < lim; j++) batch.append(files.at(j)); SearchJob* job = new SearchJob(batch, sp, rules, m_lastSearchNumber); QObject::connect(job, &SearchJob::done, this, &FileSearchTab::searchJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } void FileSearchTab::stopSearch() { #if QT_VERSION >= 0x050500 int i = m_runningJobs.size(); while (--i >= 0) - QThreadPool::globalInstance()->cancel(m_runningJobs.at(i)); + QThreadPool::globalInstance()->tryTake(m_runningJobs.at(i)); #endif m_runningJobs.clear(); } void FileSearchTab::massReplace(const QRegExp &what, const QString& with) { #define BATCH_SIZE 20 SearchResults searchResults = m_model->searchResults(); for (int i = 0; i < searchResults.count(); i += BATCH_SIZE) { int last = qMin(i + BATCH_SIZE, searchResults.count() - 1); QString filepath = searchResults.at(last).filepath; while (last + 1 < searchResults.count() && filepath == searchResults.at(last + 1).filepath) ++last; MassReplaceJob* job = new MassReplaceJob(searchResults.mid(i, last + 1 - i), i, what, with); QObject::connect(job, &MassReplaceJob::done, this, &FileSearchTab::replaceJobDone); QThreadPool::globalInstance()->start(job); m_runningJobs.append(job); } } static void copy(QTreeView* view, int column) { QApplication::clipboard()->setText(view->currentIndex().sibling(view->currentIndex().row(), column).data().toString()); } void FileSearchTab::copySourceToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Source); } void FileSearchTab::copyTargetToClipboard() { copy(ui_fileSearchOptions->treeView, FileSearchModel::Target); } void FileSearchTab::openFile() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); SearchResult sr = m_model->searchResult(item); DocPosition docPos = sr.docPos.toDocPosition(); int selection = 0; if (sr.targetPositions.size()) { docPos.offset = sr.targetPositions.first().start; selection = sr.targetPositions.first().len; } qCDebug(LOKALIZE_LOG) << "fileOpenRequest" << docPos.offset << selection; emit fileOpenRequested(sr.filepath, docPos, selection, true); } void FileSearchTab::fileSearchNext() { QModelIndex item = ui_fileSearchOptions->treeView->currentIndex(); int row = item.row(); int rowCount = m_model->rowCount(); if (++row >= rowCount) //ok if row was -1 (no solection) return; ui_fileSearchOptions->treeView->setCurrentIndex(item.sibling(row, item.column())); openFile(); } QStringList scanRecursive(const QList& urls) { QStringList result; int i = urls.size(); while (--i >= 0) { if (urls.at(i).isEmpty() || urls.at(i).path().isEmpty()) //NOTE is this a Qt bug? continue; QString path = urls.at(i).toLocalFile(); if (Catalog::extIsSupported(path)) result.append(path); else result += doScanRecursive(QDir(path)); } return result; } //returns gross number of jobs started static QStringList doScanRecursive(const QDir& dir) { QStringList result; QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable)); int i = subDirs.size(); while (--i >= 0) result += doScanRecursive(QDir(dir.filePath(subDirs.at(i)))); QStringList filters = Catalog::supportedExtensions(); i = filters.size(); while (--i >= 0) filters[i].prepend('*'); QStringList files(dir.entryList(filters, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable)); i = files.size(); while (--i >= 0) result.append(dir.filePath(files.at(i))); return result; } void FileSearchTab::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void FileSearchTab::dropEvent(QDropEvent *event) { event->acceptProposedAction(); addFilesToSearch(scanRecursive(event->mimeData()->urls())); } void FileSearchTab::addFilesToSearch(const QStringList& files) { m_searchFileListView->addFiles(files); performSearch(); } void FileSearchTab::setSourceQuery(const QString& query) { ui_fileSearchOptions->querySource->setText(query); } void FileSearchTab::setTargetQuery(const QString& query) { ui_fileSearchOptions->queryTarget->setText(query); } void FileSearchTab::searchJobDone(SearchJob* j) { j->deleteLater(); if (j->searchNumber != m_lastSearchNumber) return; /* SearchResults searchResults; FileSearchResults::const_iterator i = j->results.constBegin(); while (i != j->results.constEnd()) { foreach(const FileSearchResult& fsr, i.value()) { SearchResult sr(fsr); sr.filepath=i.key(); searchResults<appendSearchResults(searchResults); */ if (j->results.size()) { m_model->appendSearchResults(j->results); m_searchFileListView->scrollTo(j->results.last().filepath); } statusBarItems.insert(1, i18nc("@info:status message entries", "Total: %1", m_model->rowCount())); //ui_fileSearchOptions->treeView->setFocus(); } void FileSearchTab::replaceJobDone(MassReplaceJob* j) { j->deleteLater(); ui_fileSearchOptions->treeView->scrollTo(m_model->index(j->globalPos + j->searchResults.count(), 0)); } //END FileSearchTab //BEGIN MASS REPLACE MassReplaceView::MassReplaceView(QWidget* parent) : QDockWidget(i18nc("@title:window", "Mass replace"), parent) , ui(new Ui_MassReplaceOptions) { QWidget* base = new QWidget(this); setWidget(base); ui->setupUi(base); connect(ui->doPreview, &QPushButton::toggled, this, &MassReplaceView::requestPreview); connect(ui->doReplace, &QPushButton::clicked, this, &MassReplaceView::requestReplace); /* QLabel* rl=new QLabel(i18n("Replace:"), base); QLineEdit* searchEdit=new QLineEdit(base); QHBoxLayout* searchL=new QHBoxLayout(); searchL->addWidget(rl); searchL->addWidget(searchEdit); QLabel* wl=new QLabel(i18n("With:"), base); wl->setAlignment(Qt::AlignRight); wl->setMinimumSize(rl->minimumSizeHint()); QLineEdit* replacementEdit=new QLineEdit(base); QHBoxLayout* replacementL=new QHBoxLayout(); replacementL->addWidget(wl); replacementL->addWidget(replacementEdit); FlowLayout* fl=new FlowLayout(); fl->addItem(searchL); fl->addItem(replacementL); base->setLayout(fl); */ } MassReplaceView::~MassReplaceView() { delete ui; } static QRegExp regExpFromUi(const QString& s, Ui_MassReplaceOptions* ui) { return QRegExp(s, ui->matchCase->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive, ui->useRegExps->isChecked() ? QRegExp::RegExp : QRegExp::FixedString); } void MassReplaceView::requestPreviewUpdate() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.length()) ui->doReplace->setEnabled(true); emit previewRequested(regExpFromUi(s, ui), r); } void MassReplaceView::requestPreview(bool enable) { if (enable) { connect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); connect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); connect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); requestPreviewUpdate(); } else { disconnect(ui->searchText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->replaceText, &QLineEdit::textEdited, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->useRegExps, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); disconnect(ui->matchCase, &QCheckBox::toggled, this, &MassReplaceView::requestPreviewUpdate); emit previewRequested(QRegExp(), QString()); } } void MassReplaceView::requestReplace() { QString s = ui->searchText->text(); QString r = ui->replaceText->text(); if (s.isEmpty()) return; emit replaceRequested(regExpFromUi(s, ui), r); } void MassReplaceView::deactivatePreview() { ui->doPreview->setChecked(false); ui->doReplace->setEnabled(false); } #include "filesearchadaptor.h" #include QList FileSearchTab::ids; //BEGIN DBus interface QString FileSearchTab::dbusObjectPath() { QString FILESEARCH_PATH = QStringLiteral("/ThisIsWhatYouWant/FileSearch/"); if (m_dbusId == -1) { new FileSearchAdaptor(this); int i = 0; while (i < ids.size() && i == ids.at(i)) ++i; ids.insert(i, i); m_dbusId = i; QDBusConnection::sessionBus().registerObject(FILESEARCH_PATH + QString::number(m_dbusId), this); } return FILESEARCH_PATH + QString::number(m_dbusId); } bool FileSearchTab::findGuiTextPackage(QString text, QString package) { setSourceQuery(text); performSearch(); return true; } //END DBus interface diff --git a/src/tm/jobs.h b/src/tm/jobs.h index 2a42baf..0bfba38 100644 --- a/src/tm/jobs.h +++ b/src/tm/jobs.h @@ -1,494 +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() override = default; 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() override = default; 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 = default; 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/tmscanapi.cpp b/src/tm/tmscanapi.cpp index 7616c28..baeeea7 100644 --- a/src/tm/tmscanapi.cpp +++ b/src/tm/tmscanapi.cpp @@ -1,184 +1,184 @@ /* **************************************************************************** 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); + TM::threadPool()->tryTake(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) { 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()); } void RecursiveScanJob::scanJobDestroyed() { m_destroyedJobs += 1; if (m_destroyedJobs == totalAmount(KJob::Files)) { emitResult(); } } 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); QObject::connect(job, &QObject::destroyed, metaJob, &RecursiveScanJob::scanJobDestroyed); 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/tmview.cpp b/src/tm/tmview.cpp index 6e019f9..1e3735a 100644 --- a/src/tm/tmview.cpp +++ b/src/tm/tmview.cpp @@ -1,1022 +1,1022 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "tmview.h" #include "lokalize_debug.h" #include "jobs.h" #include "tmscanapi.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "dbfilesmodel.h" #include "diff.h" #include "xlifftextedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef NDEBUG #undef NDEBUG #endif #define DEBUG using namespace TM; struct DiffInfo { DiffInfo(int reserveSize); QString diffClean; QString old; //Formatting info: QByteArray diffIndex; //Map old string-->d.diffClean QVector old2DiffClean; }; DiffInfo::DiffInfo(int reserveSize) { diffClean.reserve(reserveSize); old.reserve(reserveSize); diffIndex.reserve(reserveSize); old2DiffClean.reserve(reserveSize); } /** * 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 (M appears afterwards) */ static DiffInfo getDiffInfo(const QString& diff) { DiffInfo d(diff.size()); QChar sep('{'); char state = '0'; //walk through diff string char-by-char //calculate old and others int pos = -1; while (++pos < diff.size()) { if (diff.at(pos) == sep) { if (diff.indexOf(QLatin1String("{KBABELDEL}"), pos) == pos) { state = '-'; pos += 10; } else if (diff.indexOf(QLatin1String("{KBABELADD}"), pos) == pos) { state = '+'; pos += 10; } else if (diff.indexOf(QLatin1String("{/KBABEL"), pos) == pos) { state = '0'; pos += 11; } } else { if (state != '+') { d.old.append(diff.at(pos)); d.old2DiffClean.append(d.diffIndex.count()); } d.diffIndex.append(state); d.diffClean.append(diff.at(pos)); } } return d; } void TextBrowser::mouseDoubleClickEvent(QMouseEvent* event) { QTextBrowser::mouseDoubleClickEvent(event); QString sel = textCursor().selectedText(); if (!(sel.isEmpty() || sel.contains(' '))) emit textInsertRequested(sel); } TMView::TMView(QWidget* parent, Catalog* catalog, const QVector& actions_insert, const QVector& actions_remove) : QDockWidget(i18nc("@title:window", "Translation Memory"), parent) , m_browser(new TextBrowser(this)) , m_catalog(catalog) , m_currentSelectJob(0) , m_actions_insert(actions_insert) , m_actions_remove(actions_remove) , m_normTitle(i18nc("@title:window", "Translation Memory")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_isBatching(false) , m_markAsFuzzy(false) { setObjectName(QStringLiteral("TMView")); setWidget(m_browser); m_browser->document()->setDefaultStyleSheet(QStringLiteral("p.close_match { font-weight:bold; }")); m_browser->viewport()->setBackgroundRole(QPalette::Window); QTimer::singleShot(0, this, &TMView::initLater); connect(m_catalog, QOverload::of(&Catalog::signalFileLoaded), this, &TMView::slotFileLoaded); } TMView::~TMView() { #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) - TM::threadPool()->cancel(m_jobs.at(i)); + TM::threadPool()->tryTake(m_jobs.at(i)); #endif } void TMView::initLater() { setAcceptDrops(true); int i = m_actions_insert.size(); while (--i >= 0) { connect(m_actions_insert.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); }); } i = m_actions_remove.size(); while (--i >= 0) { connect(m_actions_remove.at(i), &QAction::triggered, this, [this, i] { slotRemoveSuggestion(i); }); } setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); DBFilesModel::instance(); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &TMView::textInsertRequested); connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &TMView::contextMenu); //TODO ? kdisplayPaletteChanged // connect(KGlobalSettings::self(),,SIGNAL(kdisplayPaletteChanged()),this,SLOT(slotPaletteChanged())); } void TMView::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMView::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } void TMView::slotFileLoaded(const QString& filePath) { const QString& pID = Project::instance()->projectID(); if (Settings::scanToTMOnOpen()) TM::threadPool()->start(new ScanJob(filePath, pID), SCAN); if (!Settings::prefetchTM() && !m_isBatching) return; m_cache.clear(); #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) - TM::threadPool()->cancel(m_jobs.at(i)); + TM::threadPool()->tryTake(m_jobs.at(i)); #endif m_jobs.clear(); DocPosition pos; while (switchNext(m_catalog, pos)) { if (!m_catalog->isEmpty(pos.entry) && m_catalog->isApproved(pos.entry)) continue; SelectJob* j = initSelectJob(m_catalog, pos, pID); connect(j, &SelectJob::done, this, &TMView::slotCacheSuggestions); m_jobs.append(j); } //dummy job for the finish indication BatchSelectFinishedJob* m_seq = new BatchSelectFinishedJob(this); connect(m_seq, &BatchSelectFinishedJob::done, this, &TMView::slotBatchSelectDone); TM::threadPool()->start(m_seq, BATCHSELECTFINISHED); m_jobs.append(m_seq); } void TMView::slotCacheSuggestions(SelectJob* job) { m_jobs.removeAll(job); qCDebug(LOKALIZE_LOG) << job->m_pos.entry; if (job->m_pos.entry == m_pos.entry) slotSuggestionsCame(job); m_cache[DocPos(job->m_pos)] = job->m_entries.toVector(); } void TMView::slotBatchSelectDone() { m_jobs.clear(); if (!m_isBatching) return; bool insHappened = false; DocPosition pos; while (switchNext(m_catalog, pos)) { if (!(m_catalog->isEmpty(pos.entry) || !m_catalog->isApproved(pos.entry)) ) continue; const QVector& suggList = m_cache.value(DocPos(pos)); if (suggList.isEmpty()) continue; const TMEntry& entry = suggList.first(); if (entry.score < 9900) //hacky continue; { bool forceFuzzy = (suggList.size() > 1 && suggList.at(1).score >= 10000) || entry.score < 10000; bool ctxtMatches = entry.score == 1001; if (!m_catalog->isApproved(pos.entry)) { ///m_catalog->push(new DelTextCmd(m_catalog,pos,m_catalog->msgstr(pos))); removeTargetSubstring(m_catalog, pos, 0, m_catalog->targetWithTags(pos).string.size()); if (ctxtMatches || !(m_markAsFuzzy || forceFuzzy)) SetStateCmd::push(m_catalog, pos, true); } else if ((m_markAsFuzzy && !ctxtMatches) || forceFuzzy) { SetStateCmd::push(m_catalog, pos, false); } ///m_catalog->push(new InsTextCmd(m_catalog,pos,entry.target)); insertCatalogString(m_catalog, pos, entry.target, 0); if (Q_UNLIKELY(m_pos.entry == pos.entry && pos.form == m_pos.form)) emit refreshRequested(); } if (!insHappened) { insHappened = true; m_catalog->beginMacro(i18nc("@item Undo action", "Batch translation memory filling")); } } QString msg = i18nc("@info", "Batch translation has been completed."); if (insHappened) m_catalog->endMacro(); else { // xgettext: no-c-format msg += ' '; msg += i18nc("@info", "No suggestions with exact matches were found."); } KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation complete"), msg, this); } void TMView::slotBatchTranslate() { m_isBatching = true; m_markAsFuzzy = false; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) return slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotBatchTranslateFuzzy() { m_isBatching = true; m_markAsFuzzy = true; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotNewEntryDisplayed() { return slotNewEntryDisplayed(DocPosition()); } void TMView::slotNewEntryDisplayed(const DocPosition& pos) { if (m_catalog->numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) - TM::threadPool()->cancel(m_currentSelectJob); + TM::threadPool()->tryTake(m_currentSelectJob); #endif //update DB //m_catalog->flushUpdateDBBuffer(); //this is called via subscribtion if (pos.entry != -1) m_pos = pos; m_browser->clear(); if (Settings::prefetchTM() && m_cache.contains(DocPos(m_pos))) { QTimer::singleShot(0, this, &TMView::displayFromCache); } m_currentSelectJob = initSelectJob(m_catalog, m_pos); connect(m_currentSelectJob, &TM::SelectJob::done, this, &TMView::slotSuggestionsCame); } void TMView::displayFromCache() { if (m_prevCachePos.entry == m_pos.entry && m_prevCachePos.form == m_pos.form) return; SelectJob* temp = initSelectJob(m_catalog, m_pos, QString(), 0); temp->m_entries = m_cache.value(DocPos(m_pos)).toList(); slotSuggestionsCame(temp); temp->deleteLater(); m_prevCachePos = m_pos; } void TMView::slotSuggestionsCame(SelectJob* j) { QTime time; time.start(); SelectJob& job = *j; job.deleteLater(); if (job.m_pos.entry != m_pos.entry) return; Catalog& catalog = *m_catalog; if (catalog.numberOfEntries() <= m_pos.entry) return;//because of Qt::QueuedConnection //BEGIN query other DBs handling Project* project = Project::instance(); const QString& projectID = project->projectID(); //check if this is an additional query, from secondary DBs if (job.m_dbName != projectID) { job.m_entries += m_entries; std::sort(job.m_entries.begin(), job.m_entries.end(), std::greater()); const int limit = qMin(Settings::suggCount(), job.m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = job.m_entries.size() - 1; while (i >= 0 && (i >= limit || job.m_entries.last().score < minScore)) { job.m_entries.removeLast(); i--; } } else if (job.m_entries.isEmpty() || job.m_entries.first().score < 8500) { //be careful, as we switched to QDirModel! DBFilesModel& dbFilesModel = *(DBFilesModel::instance()); QModelIndex root = dbFilesModel.rootIndex(); int i = dbFilesModel.rowCount(root); //qCWarning(LOKALIZE_LOG)<<"query other DBs,"<= 0) { const QString& dbName = dbFilesModel.data(dbFilesModel.index(i, 0, root), DBFilesModel::NameRole).toString(); if (projectID != dbName && dbFilesModel.m_configurations.value(dbName).targetLangCode == catalog.targetLangCode()) { SelectJob* j = initSelectJob(m_catalog, m_pos, dbName); connect(j, &SelectJob::done, this, &TMView::slotSuggestionsCame); m_jobs.append(j); } } } //END query other DBs handling m_entries = job.m_entries; const int limit = job.m_entries.size(); if (!limit) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(false); m_browser->clear(); m_entryPositions.clear(); //m_entries=job.m_entries; //m_browser->insertHtml(""); int i = 0; QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); forever { QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); const TMEntry& entry = job.m_entries.at(i); html += (entry.score > 9500) ? QStringLiteral("

") : QStringLiteral("

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

") : QStringLiteral("

"); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } m_browser->insertHtml(QStringLiteral("")); setUpdatesEnabled(true); // qCWarning(LOKALIZE_LOG)<<"ELA "<document()->blockCount(); } /* void TMView::slotPaletteChanged() { }*/ bool TMView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); //int block1=m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); QMap::iterator block = m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block != m_entryPositions.end() && *block < m_entries.size()) { const TMEntry& tmEntry = m_entries.at(*block); QString file = tmEntry.file; if (file == m_catalog->url()) file = i18nc("File argument in tooltip, when file is current file", "this"); QString tooltip = i18nc("@info:tooltip", "File: %1
Addition date: %2", file, tmEntry.date.toString(Qt::ISODate)); if (!tmEntry.changeDate.isNull() && tmEntry.changeDate != tmEntry.date) tooltip += i18nc("@info:tooltip on TM entry continues", "
Last change date: %1", tmEntry.changeDate.toString(Qt::ISODate)); if (!tmEntry.changeAuthor.isEmpty()) tooltip += i18nc("@info:tooltip on TM entry continues", "
Last change author: %1", tmEntry.changeAuthor); tooltip += i18nc("@info:tooltip on TM entry continues", "
TM: %1", tmEntry.dbName); if (tmEntry.obsolete) tooltip += i18nc("@info:tooltip on TM entry continues", "
Is not present in the file anymore"); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } } return QWidget::event(event); } void TMView::removeEntry(const TMEntry& e) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this entry:
%1
from translation memory %2?", e.target.string.toHtmlEscaped(), e.dbName), i18nc("@title:window", "Translation Memory Entry Removal"))) { RemoveJob* job = new RemoveJob(e); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVE); } } void TMView::deleteFile(const TMEntry& e, const bool showPopUp) { QString filePath = e.file; if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(e.file, e.dbName); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVEFILE); if (showPopUp) { KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", e.file)); } return; } } void TMView::contextMenu(const QPoint& pos) { int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(pos).anchor()); qCWarning(LOKALIZE_LOG) << block; if (block >= m_entries.size()) return; const TMEntry& e = m_entries.at(block); enum {Remove, RemoveFile, Open}; QMenu popup; popup.addAction(i18nc("@action:inmenu", "Remove this entry"))->setData(Remove); if (e.file != m_catalog->url() && QFile::exists(e.file)) popup.addAction(i18nc("@action:inmenu", "Open file containing this entry"))->setData(Open); else { if (Settings::deleteFromTMOnMissing()) { //Automatic deletion deleteFile(e, true); } else if (!QFile::exists(e.file)) { //Still offer manual deletion if this is not the current file popup.addAction(i18nc("@action:inmenu", "Remove this missing file from TM"))->setData(RemoveFile); } } QAction* r = popup.exec(m_browser->mapToGlobal(pos)); if (!r) return; if (r->data().toInt() == Remove) { removeEntry(e); } else if (r->data().toInt() == Open) { emit fileOpenRequested(e.file, e.source.string, e.ctxt, true); } else if ((r->data().toInt() == RemoveFile) && KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this missing file:
%1
from translation memory %2?", e.file, e.dbName), i18nc("@title:window", "Translation Memory Missing File Removal"))) { deleteFile(e, false); } } /** * helper function: * searches to th nearest rxNum or ABBR * clears rxNum if ABBR is found before rxNum */ static int nextPlacableIn(const QString& old, int start, QString& cap) { static QRegExp rxNum(QStringLiteral("[\\d\\.\\%]+")); static QRegExp rxAbbr(QStringLiteral("\\w+")); int numPos = rxNum.indexIn(old, start); // int abbrPos=rxAbbr.indexIn(old,start); int abbrPos = start; //qCWarning(LOKALIZE_LOG)<<"seeing"<= 0) { if ((c++)->isUpper()) break; } abbrPos += rxAbbr.matchedLength(); } int pos = qMin(numPos, abbrPos); if (pos == -1) pos = qMax(numPos, abbrPos); // if (pos==numPos) // cap=rxNum.cap(0); // else // cap=rxAbbr.cap(0); cap = (pos == numPos ? rxNum : rxAbbr).cap(0); //qCWarning(LOKALIZE_LOG)<]*") + Settings::addColor().name() + QLatin1String("[^>]*\">([^>]*)")); QRegExp rxDel(QLatin1String("]*") + Settings::delColor().name() + QLatin1String("[^>]*\">([^>]*)")); //rxAdd.setMinimal(true); //rxDel.setMinimal(true); //first things first int pos = 0; while ((pos = rxDel.indexIn(diff, pos)) != -1) diff.replace(pos, rxDel.matchedLength(), "\tKBABELDEL\t" + rxDel.cap(1) + "\t/KBABELDEL\t"); pos = 0; while ((pos = rxAdd.indexIn(diff, pos)) != -1) diff.replace(pos, rxAdd.matchedLength(), "\tKBABELADD\t" + rxAdd.cap(1) + "\t/KBABELADD\t"); diff.replace(QStringLiteral("<"), QStringLiteral("<")); diff.replace(QStringLiteral(">"), QStringLiteral(">")); //possible enhancement: search for non-translated words in removedSubstrings... //QStringList removedSubstrings; //QStringList addedSubstrings; /* 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 */ DiffInfo d = getDiffInfo(diff); bool sameMarkup = Project::instance()->markup() == entry.markupExpr && !entry.markupExpr.isEmpty(); bool tryMarkup = !entry.target.tags.size() && sameMarkup; //search for changed markup if (tryMarkup) { QRegExp rxMarkup(entry.markupExpr); rxMarkup.setMinimal(true); pos = 0; int replacingPos = 0; while ((pos = rxMarkup.indexIn(d.old, pos)) != -1) { //qCWarning(LOKALIZE_LOG)<<"size"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 0) { QByteArray diffMPart(d.diffIndex.left(len)); int m = diffMPart.indexOf('M'); if (m != -1) diffMPart.truncate(m); #if 0 nono //first goes del, then add. so stop on second del sequence bool seenAdd = false; int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) == '+') seenAdd = true; else if (seenAdd && diffMPart.at(j) == '-') { diffMPart.truncate(j); break; } } #endif //form 'oldMarkup' QString oldMarkup; oldMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '+') oldMarkup.append(d.diffClean.at(j)); } //qCWarning(LOKALIZE_LOG)<<"old"<= 0) d.diffIndex[j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= 0) d.diffIndex[len + j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 500 cases while ((++endPos < d.diffIndex.size()) && (d.diffIndex.at(endPos) == '+') && (-1 != nextPlacableIn(QString(d.diffClean.at(endPos)), 0, _)) ) diffMPart.append('+'); qCDebug(LOKALIZE_LOG) << "diffMPart extended 1" << diffMPart; // if ((pos-1>=0) && (d.old2DiffClean.at(pos)>=0)) // { // qCWarning(LOKALIZE_LOG)<<"d.diffIndex"<= 0) && (d.diffIndex.at(startPos) == '+') //&&(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_)) ) diffMPart.prepend('+'); ++startPos; qCDebug(LOKALIZE_LOG) << "diffMPart extended 2" << diffMPart; if ((diffMPart.contains('-') || diffMPart.contains('+')) && (!diffMPart.contains('M'))) { //form newMarkup QString newMarkup; newMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '-') newMarkup.append(d.diffClean.at(startPos + j)); } if (newMarkup.endsWith(' ')) newMarkup.chop(1); //qCWarning(LOKALIZE_LOG)<<"d.old"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= m_entries.size())) return; const TMEntry& e = m_entries.at(i); removeEntry(e); } void TMView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; CatalogString target = targetAdapted(m_entries.at(i), m_catalog->sourceWithTags(m_pos)); #if 0 QString tmp = target.string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, '*'); qCWarning(LOKALIZE_LOG) << "targetAdapted" << tmp; foreach (InlineTag tag, target.tags) qCWarning(LOKALIZE_LOG) << "tag" << tag.start << tag.end; #endif if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use translation memory suggestion")); QString old = m_catalog->targetWithTags(m_pos).string; if (!old.isEmpty()) { m_pos.offset = 0; //FIXME test! removeTargetSubstring(m_catalog, m_pos, 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_pos, target, 0); if (m_entries.at(i).score > 9900 && !m_catalog->isApproved(m_pos.entry)) SetStateCmd::push(m_catalog, m_pos, true); m_catalog->endMacro(); emit refreshRequested(); }