diff --git a/src/dfm-base/base/application/application.cpp b/src/dfm-base/base/application/application.cpp index ec7c445724..10da3dfc5b 100644 --- a/src/dfm-base/base/application/application.cpp +++ b/src/dfm-base/base/application/application.cpp @@ -166,11 +166,13 @@ bool Application::syncAppAttribute() return appSetting()->sync(); } -QVariant Application::genericAttribute(Application::GenericAttribute ga) +QVariant Application::genericAttribute(Application::GenericAttribute ga, const QVariant &value) { const QString group(QT_STRINGIFY(GenericAttribute)); const QMetaEnum &me = QMetaEnum::fromType(); const QString key = QString::fromLatin1(me.valueToKey(ga)).remove(0, 1); + if (value.isValid()) + return genericSetting()->value(group, key, value); return genericSetting()->value(group, key); } diff --git a/src/dfm-base/base/application/application.h b/src/dfm-base/base/application/application.h index 4893dfab5b..8155075685 100644 --- a/src/dfm-base/base/application/application.h +++ b/src/dfm-base/base/application/application.h @@ -60,6 +60,7 @@ class Application : public QObject kShowDeleteConfirmDialog, // display the delete confirmation dialog kHideLoopPartitions, // hide loop partitions kShowThunmbnailInRemote, // show file thumbnail in remote dir + KDisplaySearchHistory, // display search history }; Q_ENUM(GenericAttribute) @@ -78,7 +79,7 @@ class Application : public QObject static void setAppAttribute(ApplicationAttribute aa, const QVariant &value); static bool syncAppAttribute(); - static QVariant genericAttribute(GenericAttribute ga); + static QVariant genericAttribute(GenericAttribute ga, const QVariant &value = QVariant()); static void setGenericAttribute(GenericAttribute ga, const QVariant &value); static bool syncGenericAttribute(); diff --git a/src/dfm-base/base/configs/settingbackend.cpp b/src/dfm-base/base/configs/settingbackend.cpp index 030dc2ee24..d783792ce2 100644 --- a/src/dfm-base/base/configs/settingbackend.cpp +++ b/src/dfm-base/base/configs/settingbackend.cpp @@ -19,6 +19,7 @@ using namespace dfmbase; #define LV2_GROUP_OPEN_ACTION "00_base.00_open_action" #define LV2_GROUP_NEW_TAB_WINDOWS "00_base.01_new_tab_windows" #define LV2_GROUP_FILES_AND_FOLDERS "00_base.02_files_and_folders" +#define LV2_GROUP_OTHERS "00_base.03_others" #define TOP_GROUP_WORKSPACE "02_workspace" #define LV2_GROUP_VIEW "02_workspace.00_view" @@ -57,6 +58,7 @@ BidirectionHash SettingBackendPrivate::k { LV2_GROUP_COMPUTER_VIEW ".01_hide_builtin_partition", Application::kHiddenSystemPartition }, { LV2_GROUP_COMPUTER_VIEW ".02_hide_loop_partitions", Application::kHideLoopPartitions }, { LV2_GROUP_COMPUTER_VIEW ".04_show_filesystemtag_on_diskicon", Application::kShowFileSystemTagOnDiskIcon }, + { LV2_GROUP_OTHERS ".00_display_search_history", Application::KDisplaySearchHistory }, }; SettingBackend::SettingBackend(QObject *parent) @@ -288,6 +290,10 @@ void SettingBackend::initBasicSettingConfig() ins->addCheckBoxConfig(LV2_GROUP_FILES_AND_FOLDERS ".02_mixed_sort", tr("Mix sorting of files and folders"), false); + ins->addGroup(LV2_GROUP_OTHERS, tr("Others")); + ins->addCheckBoxConfig(LV2_GROUP_OTHERS ".00_display_search_history", + tr("Display search history"), + true); } void SettingBackend::initWorkspaceSettingConfig() diff --git a/src/dfm-base/utils/dialogmanager.cpp b/src/dfm-base/utils/dialogmanager.cpp index 9776124f24..782a113301 100644 --- a/src/dfm-base/utils/dialogmanager.cpp +++ b/src/dfm-base/utils/dialogmanager.cpp @@ -606,6 +606,29 @@ int DialogManager::showUnableToVistDir(const QString &dir) return code; } +int DialogManager::showClearSearchHistories() +{ + QString clearSearch = tr("Are you sure clear search histories?"); + QStringList buttonTexts; + buttonTexts.append(tr("Cancel","button")); + buttonTexts.append(tr("Confirm","button")); + + DDialog d; + + if (!d.parentWidget()) { + d.setWindowFlags(d.windowFlags() | Qt::WindowStaysOnTopHint); + } + d.setIcon(QIcon::fromTheme("dialog-warning")); + d.setTitle(clearSearch); + d.addButton(buttonTexts[0], true, DDialog::ButtonNormal); + d.addButton(buttonTexts[1], false, DDialog::ButtonWarning); + d.setDefaultButton(1); + d.getButton(1)->setFocus(); + d.moveToCenter(); + int code = d.exec(); + return code; +} + DFMBASE_NAMESPACE::GlobalEventType DialogManager::showBreakSymlinkDialog(const QString &targetName, const QUrl &linkfile) { DDialog d(qApp->activeWindow()); diff --git a/src/dfm-base/utils/dialogmanager.h b/src/dfm-base/utils/dialogmanager.h index 1e5366a3e4..80c9c46817 100644 --- a/src/dfm-base/utils/dialogmanager.h +++ b/src/dfm-base/utils/dialogmanager.h @@ -75,6 +75,7 @@ class DialogManager : public QObject void showRenameBusyErrDialog(); int showRenameNameDotBeginDialog(); int showUnableToVistDir(const QString &dir); + int showClearSearchHistories(); // link file not exist DFMBASE_NAMESPACE::GlobalEventType showBreakSymlinkDialog(const QString &targetName, const QUrl &linkfile); diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/utils/titlebarhelper.cpp b/src/plugins/filemanager/core/dfmplugin-titlebar/utils/titlebarhelper.cpp index 847353efc0..a8d5679667 100644 --- a/src/plugins/filemanager/core/dfmplugin-titlebar/utils/titlebarhelper.cpp +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/utils/titlebarhelper.cpp @@ -236,6 +236,7 @@ void TitleBarHelper::handlePressed(QWidget *sender, const QString &text, bool *i search = true; fmInfo() << "search :" << text; + return; TitleBarEventCaller::sendSearch(sender, text); } } diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.cpp b/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.cpp index 7c5b48c17b..092504d51f 100644 --- a/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.cpp +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #ifdef DTKWIDGET_CLASS_DSizeMode @@ -113,6 +114,8 @@ void AddressBarPrivate::initConnect() }); connect(pauseButton, &DIconButton::clicked, q, &AddressBar::pauseButtonClicked); + connect(Application::instance(), &Application::genericAttributeChanged, + this, &AddressBarPrivate::onGenericAttributeChanged); #ifdef DTKWIDGET_CLASS_DSizeMode connect(DGuiApplicationHelper::instance(), &DGuiApplicationHelper::sizeModeChanged, this, [this]() { @@ -138,7 +141,7 @@ void AddressBarPrivate::initData() protocolIPRegExp.setPattern(R"(((smb)|(ftp)|(sftp))(://)((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3})"); protocolIPRegExp.setCaseSensitivity(Qt::CaseInsensitive); // 设置补全组件 - urlCompleter = new QCompleter(this); + urlCompleter = new DCompleter(this); setCompleter(urlCompleter); // 设置补全选择组件为popup的焦点 @@ -149,12 +152,15 @@ void AddressBarPrivate::initData() void AddressBarPrivate::updateHistory() { + ipHistroyList.clear(); + ipHistroyList = SearchHistroyManager::instance()->getIPHistory(); + + if (!Application::instance()->genericAttribute(Application::KDisplaySearchHistory, true).toBool()) + return; historyList.clear(); historyList.append(SearchHistroyManager::instance()->getSearchHistroy()); isHistoryInCompleterModel = false; - - ipHistroyList.clear(); - ipHistroyList = SearchHistroyManager::instance()->getIPHistory(); + historyList.append(QObject::tr("Clear search history")); } /*! @@ -173,7 +179,7 @@ void AddressBarPrivate::setIndicator(AddressBar::IndicatorType type) updateIndicatorIcon(); } -void AddressBarPrivate::setCompleter(QCompleter *c) +void AddressBarPrivate::setCompleter(DCompleter *c) { if (urlCompleter) { urlCompleter->disconnect(); @@ -193,10 +199,10 @@ void AddressBarPrivate::setCompleter(QCompleter *c) completerView->setItemDelegate(cpItemDelegate); completerView->setAttribute(Qt::WA_InputMethodEnabled); - connect(urlCompleter, QOverload::of(&QCompleter::activated), + connect(urlCompleter, QOverload::of(&DCompleter::activated), this, &AddressBarPrivate::insertCompletion); - connect(urlCompleter, QOverload::of(&QCompleter::highlighted), + connect(urlCompleter, QOverload::of(&DCompleter::highlighted), this, &AddressBarPrivate::onCompletionHighlighted); connect(urlCompleter->completionModel(), &QAbstractItemModel::modelReset, @@ -211,6 +217,7 @@ void AddressBarPrivate::clearCompleterModel() void AddressBarPrivate::updateCompletionState(const QString &text) { + urlCompleter->setFilterMode(Qt::MatchStartsWith); if (ipRegExp.exactMatch(text)) { inputIsIpAddress = true; completeIpAddress(text); @@ -249,7 +256,7 @@ void AddressBarPrivate::doComplete() if (completerView->isHidden()) { urlCompleter->complete(q->rect().adjusted(0, 5, 0, 5)); } else { - urlCompleter->metaObject()->invokeMethod(urlCompleter, "_q_autoResizePopup"); + urlCompleter->metaObject()->invokeMethod(urlCompleter, "onAutoResizePopup"); } if (urlCompleter->completionCount() == 1 && lastPressedKey != Qt::Key_Backspace @@ -298,6 +305,22 @@ void AddressBarPrivate::onIndicatorTriggerd() onReturnPressed(); } +void AddressBarPrivate::onGenericAttributeChanged(Application::GenericAttribute ga, const QVariant &value) +{ + if (ga != Application::KDisplaySearchHistory) + return; + auto show = value.toBool(); + if (show) { + historyList.clear(); + historyList.append(SearchHistroyManager::instance()->getSearchHistroy()); + historyList.append(QObject::tr("Clear search history")); + } else { + historyList.clear(); + completerModel.setStringList(historyList); + } + isHistoryInCompleterModel = false; +} + void AddressBarPrivate::requestCompleteByUrl(const QUrl &url) { if (!crumbController || !crumbController->isSupportedScheme(url.scheme())) { @@ -323,6 +346,7 @@ void AddressBarPrivate::requestCompleteByUrl(const QUrl &url) void AddressBarPrivate::completeSearchHistory(const QString &text) { + urlCompleter->setFilterMode(Qt::MatchRegExp); // Update Icon setIndicator(AddressBar::IndicatorType::Search); @@ -448,9 +472,15 @@ void AddressBarPrivate::onReturnPressed() // add search history list if (!dfmbase::FileUtils::isLocalFile(UrlRoute::fromUserInput(text))) { - if (!historyList.contains(text)) - historyList.removeAll(text); - historyList.append(text); + if (Application::instance()->genericAttribute(Application::KDisplaySearchHistory, true).toBool()) { + if (!historyList.contains(text)) + historyList.removeAll(text); + historyList.append(text); + historyList.removeOne(QObject::tr("Clear search history")); + historyList.append(text); + historyList.append(QObject::tr("Clear search history")); + isHistoryInCompleterModel = false; + } SearchHistroyManager::instance()->writeIntoSearchHistory(text); if (protocolIPRegExp.exactMatch(text)) { @@ -467,6 +497,13 @@ void AddressBarPrivate::onReturnPressed() } bool isSearch { false }; + if (text == QObject::tr("Clear search history")) { + emit q->escKeyPressed();; + auto result = DialogManager::instance()->showClearSearchHistories(); + if (result == DDialog::Accepted) + q->clearSearchHistory(); + return; + } TitleBarHelper::handlePressed(q, text, &isSearch); if (isSearch) { @@ -484,6 +521,13 @@ void AddressBarPrivate::insertCompletion(const QString &completion) if (inputIsIpAddress) { q->setText(completion); } else { + if (completion == tr(kClearSearchHistory)) { + isClearSearch = true; + emit q->returnPressed(); + return; + } + + isClearSearch = false; q->setText(completerBaseString + completion); } } @@ -502,7 +546,13 @@ void AddressBarPrivate::onCompletionHighlighted(const QString &highlightedComple } else { int completionPrefixLen = urlCompleter->completionPrefix().length(); int selectBeginPos = highlightedCompletion.length() - completionPrefixLen; - q->setText(completerBaseString + highlightedCompletion); + if (highlightedCompletion == QObject::tr("Clear search history")) { + q->setText(completerBaseString + lastEditedString); + isClearSearch = true; + } else { + q->setText(completerBaseString + highlightedCompletion); + isClearSearch = false; + } q->setSelection(q->text().length() - selectBeginPos, q->text().length()); } } @@ -601,6 +651,20 @@ void AddressBar::showOnFocusLostOnce() d->isKeepVisible = true; } +QString AddressBar::text() const +{ + if (d->isClearSearch && Search == d->indicatorType) + return QObject::tr("Clear search history"); + return QLineEdit::text(); +} + +void AddressBar::clearSearchHistory() +{ + d->historyList.clear(); + SearchHistroyManager::instance()->clearHistory(); + d->isHistoryInCompleterModel = false; +} + bool AddressBar::event(QEvent *e) { if (e->type() == QEvent::KeyPress) { @@ -665,9 +729,10 @@ void AddressBar::keyPressEvent(QKeyEvent *e) if (d->isHistoryInCompleterModel && e->modifiers() == Qt::ShiftModifier && e->key() == Qt::Key_Delete) { QString completeResult = d->completerView->currentIndex().data().toString(); bool ret = SearchHistroyManager::instance()->removeSearchHistory(completeResult); - if (ret) { + if (ret && Application::instance()->genericAttribute(Application::KDisplaySearchHistory, true).toBool()) { d->historyList.clear(); d->historyList.append(SearchHistroyManager::instance()->getSearchHistroy()); + d->historyList.append(QObject::tr("Clear search history")); d->completerModel.setStringList(d->historyList); } } diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.h b/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.h index 54d006de2f..5656363031 100644 --- a/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.h +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/addressbar.h @@ -29,6 +29,8 @@ class AddressBar : public QLineEdit void setCurrentUrl(const QUrl &url); QUrl currentUrl(); void showOnFocusLostOnce(); + QString text() const; + void clearSearchHistory(); protected: bool event(QEvent *e) override; diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.cpp b/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.cpp new file mode 100644 index 0000000000..12167864f5 --- /dev/null +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.cpp @@ -0,0 +1,1737 @@ +// SPDX-FileCopyrightText: 2021 - 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#include "views/dcompleter.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +DCompletionEngine::~DCompletionEngine() +{ + +} + +//////////////////////////////////////////////////////////////////////////////////////// +void DCompletionEngine::filter(const QStringList& parts) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + curParts = parts; + if (curParts.isEmpty()) + curParts.append(QString()); + + curRow = -1; + curParent = QModelIndex(); + curMatch = DMatchData(); + historyMatch = filterHistory(); + + if (!model) + return; + + QModelIndex parent; + for (int i = 0; i < curParts.count() - 1; i++) { + QString part = curParts.at(i); + int emi = filter(part, parent, -1).exactMatchIndex; + if (emi == -1) + return; + parent = model->index(emi, c->column, parent); + } + + // Note that we set the curParent to a valid parent, even if we have no matches + // When filtering is disabled, we show all the items under this parent + curParent = parent; + if (curParts.constLast().isEmpty()) + curMatch = DMatchData(DIndexMapper(0, model->rowCount(curParent) - 1), -1, false); + else + curMatch = filter(curParts.constLast(), curParent, 1); // build at least one + curRow = curMatch.isValid() ? 0 : -1; +} + +DMatchData DCompletionEngine::filterHistory() +{ + QAbstractItemModel *source = c->proxy->sourceModel(); + if (curParts.count() <= 1 || c->proxy->showAll || !source) + return DMatchData(); + +#if QT_CONFIG(dirmodel) + const bool isDirModel = (qobject_cast(source) != nullptr); +#else + const bool isDirModel = false; +#endif + Q_UNUSED(isDirModel) +#if QT_CONFIG(filesystemmodel) + const bool isFsModel = (qobject_cast(source) != nullptr); +#else + const bool isFsModel = false; +#endif + Q_UNUSED(isFsModel) + QVector v; + DIndexMapper im(v); + DMatchData m(im, -1, true); + + for (int i = 0; i < source->rowCount(); i++) { + QString str = source->index(i, c->column).data().toString(); + if (str.startsWith(c->prefix, c->cs) +#if !defined(Q_OS_WIN) + && ((!isFsModel && !isDirModel) || QDir::toNativeSeparators(str) != QDir::separator()) +#endif + ) + m.indices.append(i); + } + return m; +} + +// Returns a match hint from the cache by chopping the search string +bool DCompletionEngine::matchHint(const QString &part, const QModelIndex &parent, DMatchData *hint) const +{ + if (part.isEmpty()) + return false; // early out to avoid cache[parent] lookup costs + + const auto cit = cache.find(parent); + if (cit == cache.end()) + return false; + + const CacheItem& map = *cit; + const auto mapEnd = map.end(); + + QString key = c->cs == Qt::CaseInsensitive ? part.toLower() : part; + + while (!key.isEmpty()) { + key.chop(1); + const auto it = map.find(key); + if (it != mapEnd) { + *hint = *it; + return true; + } + } + + return false; +} + +// When the cache size exceeds 1MB, it clears out about 1/2 of the cache. +void DCompletionEngine::saveInCache(QString part, const QModelIndex& parent, const DMatchData& m) +{ + if (c->filterModes == Qt::MatchEndsWith) + return; + DMatchData old = cache[parent].take(part); + cost = cost + m.indices.cost() - old.indices.cost(); + if (cost * static_cast(sizeof(int)) > 1024 * 1024) { + QMap::iterator it1 = cache.begin(); + while (it1 != cache.end()) { + CacheItem& ci = it1.value(); + int sz = ci.count()/2; + QMap::iterator it2 = ci.begin(); + int i = 0; + while (it2 != ci.end() && i < sz) { + cost -= it2.value().indices.cost(); + it2 = ci.erase(it2); + i++; + } + if (ci.count() == 0) { + it1 = cache.erase(it1); + } else { + ++it1; + } + } + } + + if (c->cs == Qt::CaseInsensitive) + part = std::move(part).toLower(); + cache[parent][part] = m; +} + +bool DCompletionEngine::lookupCache(const QString &part, const QModelIndex &parent, DMatchData *m) const +{ + if (part.isEmpty()) + return false; // early out to avoid cache[parent] lookup costs + + const auto cit = cache.find(parent); + if (cit == cache.end()) + return false; + + const CacheItem& map = *cit; + + const QString key = c->cs == Qt::CaseInsensitive ? part.toLower() : part; + + const auto it = map.find(key); + if (it == map.end()) + return false; + + *m = it.value(); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////// +DIndexMapper DSortedModelEngine::indexHint(QString part, const QModelIndex& parent, Qt::SortOrder order) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + if (c->cs == Qt::CaseInsensitive) + part = std::move(part).toLower(); + + const CacheItem& map = cache[parent]; + + // Try to find a lower and upper bound for the search from previous results + int to = model->rowCount(parent) - 1; + int from = 0; + const CacheItem::const_iterator it = map.lowerBound(part); + + // look backward for first valid hint + for(CacheItem::const_iterator it1 = it; it1-- != map.constBegin();) { + const DMatchData& value = it1.value(); + if (value.isValid()) { + if (order == Qt::AscendingOrder) { + from = value.indices.last() + 1; + } else { + to = value.indices.first() - 1; + } + break; + } + } + + // look forward for first valid hint + for(CacheItem::const_iterator it2 = it; it2 != map.constEnd(); ++it2) { + const DMatchData& value = it2.value(); + if (value.isValid() && !it2.key().startsWith(part)) { + if (order == Qt::AscendingOrder) { + to = value.indices.first() - 1; + } else { + from = value.indices.first() + 1; + } + break; + } + } + + return DIndexMapper(from, to); +} + +Qt::SortOrder DSortedModelEngine::sortOrder(const QModelIndex &parent) const +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + int rowCount = model->rowCount(parent); + if (rowCount < 2) + return Qt::AscendingOrder; + QString first = model->data(model->index(0, c->column, parent), c->role).toString(); + QString last = model->data(model->index(rowCount - 1, c->column, parent), c->role).toString(); + return QString::compare(first, last, c->cs) <= 0 ? Qt::AscendingOrder : Qt::DescendingOrder; +} + +DMatchData DSortedModelEngine::filter(const QString& part, const QModelIndex& parent, int) +{ + const QAbstractItemModel *model = c->proxy->sourceModel(); + + DMatchData hint; + if (lookupCache(part, parent, &hint)) + return hint; + + DIndexMapper indices; + Qt::SortOrder order = sortOrder(parent); + + if (matchHint(part, parent, &hint)) { + if (!hint.isValid()) + return DMatchData(); + indices = hint.indices; + } else { + indices = indexHint(part, parent, order); + } + + // binary search the model within 'indices' for 'part' under 'parent' + int high = indices.to() + 1; + int low = indices.from() - 1; + int probe; + QModelIndex probeIndex; + QString probeData; + + while (high - low > 1) + { + probe = (high + low) / 2; + probeIndex = model->index(probe, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + const int cmp = QString::compare(probeData, part, c->cs); + if ((order == Qt::AscendingOrder && cmp >= 0) + || (order == Qt::DescendingOrder && cmp < 0)) { + high = probe; + } else { + low = probe; + } + } + + if ((order == Qt::AscendingOrder && low == indices.to()) + || (order == Qt::DescendingOrder && high == indices.from())) { // not found + saveInCache(part, parent, DMatchData()); + return DMatchData(); + } + + probeIndex = model->index(order == Qt::AscendingOrder ? low+1 : high-1, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + if (!probeData.startsWith(part, c->cs)) { + saveInCache(part, parent, DMatchData()); + return DMatchData(); + } + + const bool exactMatch = QString::compare(probeData, part, c->cs) == 0; + int emi = exactMatch ? (order == Qt::AscendingOrder ? low+1 : high-1) : -1; + + int from = 0; + int to = 0; + if (order == Qt::AscendingOrder) { + from = low + 1; + high = indices.to() + 1; + low = from; + } else { + to = high - 1; + low = indices.from() - 1; + high = to; + } + + while (high - low > 1) + { + probe = (high + low) / 2; + probeIndex = model->index(probe, c->column, parent); + probeData = model->data(probeIndex, c->role).toString(); + const bool startsWith = probeData.startsWith(part, c->cs); + if ((order == Qt::AscendingOrder && startsWith) + || (order == Qt::DescendingOrder && !startsWith)) { + low = probe; + } else { + high = probe; + } + } + + DMatchData m(order == Qt::AscendingOrder ? DIndexMapper(from, high - 1) : DIndexMapper(low+1, to), emi, false); + saveInCache(part, parent, m); + return m; +} + +//////////////////////////////////////////////////////////////////////////////////////// +void DUnsortedModelEngine::filterOnDemand(int n) +{ + Q_ASSERT(matchCount()); + if (!curMatch.partial) + return; + Q_ASSERT(n >= -1); + const QAbstractItemModel *model = c->proxy->sourceModel(); + int lastRow = model->rowCount(curParent) - 1; + DIndexMapper im(curMatch.indices.last() + 1, lastRow); + int lastIndex = buildIndices(curParts.constLast(), curParent, n, im, &curMatch); + curMatch.partial = (lastRow != lastIndex); + saveInCache(curParts.constLast(), curParent, curMatch); +} + +DMatchData DUnsortedModelEngine::filter(const QString& part, const QModelIndex& parent, int n) +{ + DMatchData hint; + + QVector v; + DIndexMapper im(v); + DMatchData m(im, -1, true); + + const QAbstractItemModel *model = c->proxy->sourceModel(); + bool foundInCache = lookupCache(part, parent, &m); + + if (!foundInCache) { + if (matchHint(part, parent, &hint) && !hint.isValid()) + return DMatchData(); + } + + if (!foundInCache && !hint.isValid()) { + const int lastRow = model->rowCount(parent) - 1; + DIndexMapper all(0, lastRow); + int lastIndex = buildIndices(part, parent, n, all, &m); + m.partial = (lastIndex != lastRow); + } else { + if (!foundInCache) { // build from hint as much as we can + buildIndices(part, parent, INT_MAX, hint.indices, &m); + m.partial = hint.partial; + } + if (m.partial && ((n == -1 && m.exactMatchIndex == -1) || (m.indices.count() < n))) { + // need more and have more + const int lastRow = model->rowCount(parent) - 1; + DIndexMapper rest(hint.indices.last() + 1, lastRow); + int want = n == -1 ? -1 : n - m.indices.count(); + int lastIndex = buildIndices(part, parent, want, rest, &m); + m.partial = (lastRow != lastIndex); + } + } + + saveInCache(part, parent, m); + return m; +} + +int DUnsortedModelEngine::buildIndices(const QString& str, const QModelIndex& parent, int n, + const DIndexMapper& indices, DMatchData* m) +{ + Q_ASSERT(m->partial); + Q_ASSERT(n != -1 || m->exactMatchIndex == -1); + const QAbstractItemModel *model = c->proxy->sourceModel(); + int i, count = 0, clearData = -1; + + for (i = 0; i < indices.count() && count != n; ++i) { + QModelIndex idx = model->index(indices[i], c->column, parent); + + if (!(model->flags(idx) & Qt::ItemIsSelectable)) + continue; + + QString data = model->data(idx, c->role).toString(); + + switch (c->filterModes) { + case Qt::MatchStartsWith: + if (!data.startsWith(str, c->cs)) + continue; + break; + case Qt::MatchContains: + if (!data.contains(str, c->cs)) + continue; + break; + case Qt::MatchEndsWith: + if (!data.endsWith(str, c->cs)) + continue; + break; + case Qt::MatchRegExp: + { + if (data == QObject::tr("Clear search history")) { + clearData = indices[i]; + continue; + } + if (!data.startsWith(str, c->cs)) + continue; + break; + } + case Qt::MatchExactly: + case Qt::MatchFixedString: + case Qt::MatchCaseSensitive: + case Qt::MatchWildcard: + case Qt::MatchWrap: + case Qt::MatchRecursive: + Q_UNREACHABLE(); + break; + } + m->indices.append(indices[i]); + ++count; + if (m->exactMatchIndex == -1 && QString::compare(data, str, c->cs) == 0) { + m->exactMatchIndex = indices[i]; + if (n == -1) + return indices[i]; + } + } + if (clearData != -1 && m->indices.count() >= 1) + m->indices.append(clearData); + return indices[i-1]; +} + +//////////////////////////////////////////////////////////////////////////////////////// +void DCompleterItemDelegate::paint(QPainter *p, const QStyleOptionViewItem &opt, const QModelIndex &idx) const +{ + QStyleOptionViewItem optCopy = opt; + optCopy.showDecorationSelected = true; + if (view->currentIndex() == idx) + optCopy.state |= QStyle::State_HasFocus; + QItemDelegate::paint(p, optCopy, idx); +} + +//////////////////////////////////////////////////////////////////////////////////////// +DCompletionModel::DCompletionModel(DCompleter *c, QObject *parent) + : QAbstractProxyModel(parent), + c(c), showAll(false) +{ + createEngine(); +} + +int DCompletionModel::columnCount(const QModelIndex &) const +{ + if (sourceModel()) + return sourceModel()->columnCount(); + return 0; +} + +void DCompletionModel::setSourceModel(QAbstractItemModel *source) +{ + bool hadModel = (sourceModel() != nullptr); + + if (hadModel) + QObject::disconnect(sourceModel(), nullptr, this, nullptr); + + QAbstractProxyModel::setSourceModel(source); + + if (source) { + // TODO: Optimize updates in the source model + connect(source, SIGNAL(modelReset()), this, SLOT(invalidate())); + connect(source, SIGNAL(destroyed()), this, SLOT(modelDestroyed())); + connect(source, SIGNAL(layoutChanged()), this, SLOT(invalidate())); + connect(source, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted())); + connect(source, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(invalidate())); + connect(source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(invalidate())); + } + + invalidate(); +} + +void DCompletionModel::createEngine() +{ + bool sortedEngine = false; + if (c->filterModes == Qt::MatchStartsWith) { + switch (c->sorting) { + case QCompleter::UnsortedModel: + sortedEngine = false; + break; + case QCompleter::CaseSensitivelySortedModel: + sortedEngine = c->cs == Qt::CaseSensitive; + break; + case QCompleter::CaseInsensitivelySortedModel: + sortedEngine = c->cs == Qt::CaseInsensitive; + break; + } + } + + if (sortedEngine) + engine.reset(new DSortedModelEngine(c)); + else + engine.reset(new DUnsortedModelEngine(c)); +} + +QModelIndex DCompletionModel::mapToSource(const QModelIndex& index) const +{ + if (!index.isValid()) + return engine->curParent; + + int row; + QModelIndex parent = engine->curParent; + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + Q_ASSERT(index.row() < engine->matchCount()); + DIndexMapper& rootIndices = engine->historyMatch.indices; + if (index.row() < rootIndices.count()) { + row = rootIndices[index.row()]; + parent = QModelIndex(); + } else { + row = engine->curMatch.indices[index.row() - rootIndices.count()]; + } + } else { + row = index.row(); + } + + return sourceModel()->index(row, index.column(), parent); +} + +QModelIndex DCompletionModel::mapFromSource(const QModelIndex& idx) const +{ + if (!idx.isValid()) + return QModelIndex(); + + int row = -1; + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + + DIndexMapper& rootIndices = engine->historyMatch.indices; + if (idx.parent().isValid()) { + if (idx.parent() != engine->curParent) + return QModelIndex(); + } else { + row = rootIndices.indexOf(idx.row()); + if (row == -1 && engine->curParent.isValid()) + return QModelIndex(); // source parent and our parent don't match + } + + if (row == -1) { + DIndexMapper& indices = engine->curMatch.indices; + engine->filterOnDemand(idx.row() - indices.last()); + row = indices.indexOf(idx.row()) + rootIndices.count(); + } + + if (row == -1) + return QModelIndex(); + } else { + if (idx.parent() != engine->curParent) + return QModelIndex(); + row = idx.row(); + } + + return createIndex(row, idx.column()); +} + +bool DCompletionModel::setCurrentRow(int row) +{ + if (row < 0 || !engine->matchCount()) + return false; + + if (row >= engine->matchCount()) + engine->filterOnDemand(row + 1 - engine->matchCount()); + + if (row >= engine->matchCount()) // invalid row + return false; + + engine->curRow = row; + return true; +} + +QModelIndex DCompletionModel::currentIndex(bool sourceIndex) const +{ + if (!engine->matchCount()) + return QModelIndex(); + + int row = engine->curRow; + if (showAll) + row = engine->curMatch.indices[engine->curRow]; + + QModelIndex idx = createIndex(row, c->column); + if (!sourceIndex) + return idx; + return mapToSource(idx); +} + +QModelIndex DCompletionModel::index(int row, int column, const QModelIndex& parent) const +{ + if (row < 0 || column < 0 || column >= columnCount(parent) || parent.isValid()) + return QModelIndex(); + + if (!showAll) { + if (!engine->matchCount()) + return QModelIndex(); + if (row >= engine->historyMatch.indices.count()) { + int want = row + 1 - engine->matchCount(); + if (want > 0) + engine->filterOnDemand(want); + if (row >= engine->matchCount()) + return QModelIndex(); + } + } else { + if (row >= sourceModel()->rowCount(engine->curParent)) + return QModelIndex(); + } + + return createIndex(row, column); +} + +int DCompletionModel::completionCount() const +{ + if (!engine->matchCount()) + return 0; + + engine->filterOnDemand(INT_MAX); + return engine->matchCount(); +} + +int DCompletionModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + if (showAll) { + // Show all items below current parent, even if we have no valid matches + if (engine->curParts.count() != 1 && !engine->matchCount() + && !engine->curParent.isValid()) + return 0; + return sourceModel()->rowCount(engine->curParent); + } + + return completionCount(); +} + +void DCompletionModel::setFiltered(bool filtered) +{ + if (showAll == !filtered) + return; + beginResetModel(); + showAll = !filtered; + endResetModel(); +} + +bool DCompletionModel::hasChildren(const QModelIndex &parent) const +{ + if (parent.isValid()) + return false; + + if (showAll) + return sourceModel()->hasChildren(mapToSource(parent)); + + if (!engine->matchCount()) + return false; + + return true; +} + +QVariant DCompletionModel::data(const QModelIndex& index, int role) const +{ + return sourceModel()->data(mapToSource(index), role); +} + +void DCompletionModel::modelDestroyed() +{ + QAbstractProxyModel::setSourceModel(nullptr); // switch to static empty model + invalidate(); +} + +void DCompletionModel::rowsInserted() +{ + invalidate(); + emit rowsAdded(); +} + +void DCompletionModel::invalidate() +{ + engine->cache.clear(); + filter(engine->curParts); +} + +void DCompletionModel::filter(const QStringList& parts) +{ + beginResetModel(); + engine->filter(parts); + endResetModel(); + + if (!sourceModel()) + return; + if (sourceModel()->canFetchMore(engine->curParent)) + sourceModel()->fetchMore(engine->curParent); +} + + +/////////////////////////////////////////////////////////////////////////////// +/*! + Constructs a completer object with the given \a parent. +*/ +DCompleter::DCompleter(QObject *parent) +: QObject(parent), widgetptr(nullptr), proxy(nullptr), popupA(nullptr), filterModes(Qt::MatchStartsWith), cs(Qt::CaseSensitive), + role(Qt::EditRole), column(0), maxVisibleItemsCount(7), sorting(QCompleter::UnsortedModel), + wrap(true), eatFocusOut(true), hiddenBecauseNoMatch(false) +{ + init(); +} + +/*! + Constructs a completer object with the given \a parent that provides completions + from the specified \a model. +*/ +DCompleter::DCompleter(QAbstractItemModel *model, QObject *parent) + : QObject(parent), widgetptr(nullptr), proxy(nullptr), popupA(nullptr), filterModes(Qt::MatchStartsWith), cs(Qt::CaseSensitive), + role(Qt::EditRole), column(0), maxVisibleItemsCount(7), sorting(QCompleter::UnsortedModel), + wrap(true), eatFocusOut(true), hiddenBecauseNoMatch(false) +{ + init(model); +} + +#ifndef QT_NO_STRINGLISTMODEL +/*! + Constructs a QCompleter object with the given \a parent that uses the specified + \a list as a source of possible completions. +*/ +DCompleter::DCompleter(const QStringList& list, QObject *parent) +: QObject(parent), widgetptr(nullptr), proxy(nullptr), popupA(nullptr), filterModes(Qt::MatchStartsWith), cs(Qt::CaseSensitive), + role(Qt::EditRole), column(0), maxVisibleItemsCount(7), sorting(QCompleter::UnsortedModel), + wrap(true), eatFocusOut(true), hiddenBecauseNoMatch(false) +{ + init(new QStringListModel(list, this)); +} +#endif // QT_NO_STRINGLISTMODEL + +/*! + Destroys the completer object. +*/ +DCompleter::~DCompleter() +{ + if (popupA) { + delete popupA; + popupA = nullptr; + } +} + +/*! + Sets the widget for which completion are provided for to \a widget. This + function is automatically called when a QCompleter is set on a QLineEdit + using QLineEdit::setCompleter() or on a QComboBox using + QComboBox::setCompleter(). The widget needs to be set explicitly when + providing completions for custom widgets. + + \sa widget(), setModel(), setPopup() + */ +void DCompleter::setWidget(QWidget *widget) +{ + if (widget == widgetptr) + return; + + if (widgetptr) + widgetptr->removeEventFilter(this); + widgetptr = widget; + if (widgetptr) + widgetptr->installEventFilter(this); + + if (popupA) { + popupA->hide(); + popupA->setFocusProxy(widgetptr); + } +} + +/*! + Returns the widget for which the completer object is providing completions. + + \sa setWidget() + */ +QWidget *DCompleter::widget() const +{ + return widgetptr; +} + +/*! + Sets the model which provides completions to \a model. The \a model can + be list model or a tree model. If a model has been already previously set + and it has the QCompleter as its parent, it is deleted. + + For convenience, if \a model is a QFileSystemModel, QCompleter switches its + caseSensitivity to Qt::CaseInsensitive on Windows and Qt::CaseSensitive + on other platforms. + + \sa completionModel(), modelSorting, {Handling Tree Models} +*/ +void DCompleter::setModel(QAbstractItemModel *model) +{ + QAbstractItemModel *oldModel = proxy->sourceModel(); +#if QT_CONFIG(filesystemmodel) + if (oldModel && qobject_cast(oldModel)) + setCompletionRole(Qt::EditRole); // QTBUG-54642, clear FileNameRole set by QFileSystemModel +#endif + proxy->setSourceModel(model); + if (popupA) + setPopup(popupA); // set the model and make new connections + if (oldModel && oldModel->QObject::parent() == this) + delete oldModel; +#if QT_CONFIG(dirmodel) + if (qobject_cast(model)) { +#if defined(Q_OS_WIN) + setCaseSensitivity(Qt::CaseInsensitive); +#else + setCaseSensitivity(Qt::CaseSensitive); +#endif + } +#endif // QT_CONFIG(dirmodel) +#if QT_CONFIG(filesystemmodel) + QFileSystemModel *fsModel = qobject_cast(model); + if (fsModel) { +#if defined(Q_OS_WIN) + setCaseSensitivity(Qt::CaseInsensitive); +#else + setCaseSensitivity(Qt::CaseSensitive); +#endif + setCompletionRole(QFileSystemModel::FileNameRole); + connect(fsModel, &QFileSystemModel::directoryLoaded, this, &DCompleter::onFileSystemModelDirectoryLoaded); + } +#endif // QT_CONFIG(filesystemmodel) +} + +/*! + Returns the model that provides completion strings. + + \sa completionModel() +*/ +QAbstractItemModel *DCompleter::model() const +{ + return proxy->sourceModel(); +} + +/*! + \enum DCompleter::CompletionMode + + This enum specifies how completions are provided to the user. + + \value PopupCompletion Current completions are displayed in a popup window. + \value InlineCompletion Completions appear inline (as selected text). + \value UnfilteredPopupCompletion All possible completions are displayed in a popup window with the most likely suggestion indicated as current. + + \sa setCompletionMode() +*/ + +/*! + \property DCompleter::completionMode + \brief how the completions are provided to the user + + The default value is DCompleter::PopupCompletion. +*/ +void DCompleter::setCompletionMode(QCompleter::CompletionMode mode) +{ + this->mode = mode; + proxy->setFiltered(mode != QCompleter::UnfilteredPopupCompletion); + + if (mode == QCompleter::InlineCompletion) { + if (widgetptr) + widgetptr->removeEventFilter(this); + if (popupA) { + popupA->deleteLater(); + popupA = nullptr; + } + } else { + if (widgetptr) + widgetptr->installEventFilter(this); + } +} + +QCompleter::CompletionMode DCompleter::completionMode() const +{ + return mode; +} + +/*! + \property DCompleter::filterMode + \brief how the filtering is performed + \since 5.2 + + If filterMode is set to Qt::MatchStartsWith, only those entries that start + with the typed characters will be displayed. Qt::MatchContains will display + the entries that contain the typed characters, and Qt::MatchEndsWith the + ones that end with the typed characters. + + Currently, only these three modes are implemented. Setting filterMode to + any other Qt::MatchFlag will issue a warning, and no action will be + performed. + + The default mode is Qt::MatchStartsWith. +*/ + +void DCompleter::setFilterMode(Qt::MatchFlags filterMode) +{ + if (filterModes == filterMode) + return; + + if (Q_UNLIKELY(filterMode != Qt::MatchStartsWith && + filterMode != Qt::MatchContains && + filterMode != Qt::MatchEndsWith && + filterMode != Qt::MatchRegExp)) { + qWarning("Unhandled DCompleter::filterMode flag is used."); + return; + } + + filterModes = filterMode; + proxy->createEngine(); + proxy->invalidate(); +} + +Qt::MatchFlags DCompleter::filterMode() const +{ + return filterModes; +} + +/*! + Sets the popup used to display completions to \a popup. QCompleter takes + ownership of the view. + + A QListView is automatically created when the completionMode() is set to + DCompleter::PopupCompletion or DCompleter::UnfilteredPopupCompletion. The + default popup displays the completionColumn(). + + Ensure that this function is called before the view settings are modified. + This is required since view's properties may require that a model has been + set on the view (for example, hiding columns in the view requires a model + to be set on the view). + + \sa popup() +*/ +void DCompleter::setPopup(QAbstractItemView *popup) +{ + Q_ASSERT(popup != nullptr); + if (popupA) { + QObject::disconnect(popupA->selectionModel(), nullptr, this, nullptr); + QObject::disconnect(popupA, nullptr, this, nullptr); + } + if (popupA != popup) + delete popupA; + if (popup->model() != proxy) + popup->setModel(proxy); + popup->hide(); + + Qt::FocusPolicy origPolicy = Qt::NoFocus; + if (widgetptr) + origPolicy = widgetptr->focusPolicy(); + + // Mark the widget window as a popup, so that if the last non-popup window is closed by the + // user, the application should not be prevented from exiting. It needs to be set explicitly via + // setWindowFlag(), because passing the flag via setParent(parent, windowFlags) does not call + // QWidgetPrivate::adjustQuitOnCloseAttribute(), and causes an application not to exit if the + // popup ends up being the last window. + popup->setParent(nullptr); + popup->setWindowFlag(Qt::Popup); + popup->setFocusPolicy(Qt::NoFocus); + if (widgetptr) + widgetptr->setFocusPolicy(origPolicy); + + popup->setFocusProxy(widgetptr); + popup->installEventFilter(this); + popup->setItemDelegate(new DCompleterItemDelegate(popup)); +#if QT_CONFIG(listview) + if (QListView *listView = qobject_cast(popup)) { + listView->setModelColumn(column); + } +#endif + + QObject::connect(popup, &QAbstractItemView::clicked, + this, &DCompleter::onComplete); + QObject::connect(this, SIGNAL(activated(QModelIndex)), + popup, SLOT(hide())); + + QObject::connect(popup->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &DCompleter::onCompletionSelected); + popupA = popup; +} + +/*! + Returns the popup used to display completions. + + \sa setPopup() +*/ +QAbstractItemView *DCompleter::popup() const +{ + if (!popupA && completionMode() != QCompleter::InlineCompletion) { + QListView *listView = new QListView; + listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView->setSelectionBehavior(QAbstractItemView::SelectRows); + listView->setSelectionMode(QAbstractItemView::SingleSelection); + listView->setModelColumn(column); + DCompleter *that = const_cast(this); + that->setPopup(listView); + } + return popupA; +} + +/*! + \reimp +*/ +bool DCompleter::event(QEvent *ev) +{ + return QObject::event(ev); +} + +void DCompleter::onComplete(QModelIndex index) +{ + completed(index); +} + +void DCompleter::onCompletionSelected(const QItemSelection &s) +{ + completionSelected(s); +} + +void DCompleter::onAutoResizePopup() +{ + autoResizePopup(); +} + +void DCompleter::onFileSystemModelDirectoryLoaded(const QString &st) +{ + fileSystemModelDirectoryLoaded(st); +} + +/*! + \reimp +*/ +bool DCompleter::eventFilter(QObject *o, QEvent *e) +{ + if (eatFocusOut && o == widgetptr && e->type() == QEvent::FocusOut) { + hiddenBecauseNoMatch = false; + if (popupA && popupA->isVisible()) + return true; + } + + if (o != popupA) + return QObject::eventFilter(o, e); + + switch (e->type()) { + case QEvent::KeyPress: { + QKeyEvent *ke = static_cast(e); + + QModelIndex curIndex = popupA->currentIndex(); + QModelIndexList selList = popupA->selectionModel()->selectedIndexes(); + + const int key = ke->key(); + // In UnFilteredPopup mode, select the current item + if ((key == Qt::Key_Up || key == Qt::Key_Down) && selList.isEmpty() && curIndex.isValid() + && mode == QCompleter::UnfilteredPopupCompletion) { + setCurrentIndex(curIndex); + return true; + } + + // Handle popup navigation keys. These are hardcoded because up/down might make the + // widget do something else (lineedit cursor moves to home/end on mac, for instance) + switch (key) { + case Qt::Key_End: + case Qt::Key_Home: + if (ke->modifiers() & Qt::ControlModifier) + return false; + break; + + case Qt::Key_Up: + if (!curIndex.isValid()) { + int rowCount = proxy->rowCount(); + QModelIndex lastIndex = proxy->index(rowCount - 1, column); + setCurrentIndex(lastIndex); + return true; + } else if (curIndex.row() == 0) { + if (wrap) + setCurrentIndex(QModelIndex()); + return true; + } + return false; + + case Qt::Key_Down: + if (!curIndex.isValid()) { + QModelIndex firstIndex = proxy->index(0, column); + setCurrentIndex(firstIndex); + return true; + } else if (curIndex.row() == proxy->rowCount() - 1) { + if (wrap) + setCurrentIndex(QModelIndex()); + return true; + } + return false; + + case Qt::Key_PageUp: + case Qt::Key_PageDown: + return false; + } + + // Send the event to the widget. If the widget accepted the event, do nothing + // If the widget did not accept the event, provide a default implementation + eatFocusOut = false; + (static_cast(widgetptr))->event(ke); + eatFocusOut = true; + if (!widgetptr || e->isAccepted() || !popupA->isVisible()) { + // widget lost focus, hide the popup + if (widgetptr && (!widgetptr->hasFocus() +#ifdef QT_KEYPAD_NAVIGATION + || (QApplication::keypadNavigationEnabled() && !widgetptr->hasEditFocus()) +#endif + )) + popupA->hide(); + if (e->isAccepted()) + return true; + } + + // default implementation for keys not handled by the widget when popup is open +#if QT_CONFIG(shortcut) + if (ke->matches(QKeySequence::Cancel)) { + popupA->hide(); + return true; + } +#endif + switch (key) { +#ifdef QT_KEYPAD_NAVIGATION + case Qt::Key_Select: + if (!QApplication::keypadNavigationEnabled()) + break; +#endif + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Tab: + popupA->hide(); + if (curIndex.isValid()) + completed(curIndex); + break; + + case Qt::Key_F4: + if (ke->modifiers() & Qt::AltModifier) + popupA->hide(); + break; + + case Qt::Key_Backtab: + popupA->hide(); + break; + + default: + break; + } + + return true; + } + +#ifdef QT_KEYPAD_NAVIGATION + case QEvent::KeyRelease: { + QKeyEvent *ke = static_cast(e); + if (QApplication::keypadNavigationEnabled() && ke->key() == Qt::Key_Back) { + // Send the event to the 'widget'. This is what we did for KeyPress, so we need + // to do the same for KeyRelease, in case the widget's KeyPress event set + // up something (such as a timer) that is relying on also receiving the + // key release. I see this as a bug in Qt, and should really set it up for all + // the affected keys. However, it is difficult to tell how this will affect + // existing code, and I can't test for every combination! + d->eatFocusOut = false; + static_cast(widgetptr)->event(ke); + d->eatFocusOut = true; + } + break; + } +#endif + + case QEvent::MouseButtonPress: { +#ifdef QT_KEYPAD_NAVIGATION + if (QApplication::keypadNavigationEnabled()) { + // if we've clicked in the widget (or its descendant), let it handle the click + QWidget *source = qobject_cast(o); + if (source) { + QPoint pos = source->mapToGlobal((static_cast(e))->pos()); + QWidget *target = QApplication::widgetAt(pos); + if (target && (widgetptr->isAncestorOf(target) || + target == widgetptr)) { + d->eatFocusOut = false; + static_cast(target)->event(e); + d->eatFocusOut = true; + return true; + } + } + } +#endif + if (!popupA->underMouse()) { + popupA->hide(); + return true; + } + } + return false; + + case QEvent::InputMethod: + case QEvent::ShortcutOverride: + QApplication::sendEvent(widgetptr, e); + break; + + default: + return false; + } + return false; +} + +/*! + For DCompleter::PopupCompletion and QCompletion::UnfilteredPopupCompletion + modes, calling this function displays the popup displaying the current + completions. By default, if \a rect is not specified, the popup is displayed + on the bottom of the widget(). If \a rect is specified the popup is + displayed on the left edge of the rectangle. + + For DCompleter::InlineCompletion mode, the highlighted() signal is fired + with the current completion. +*/ +void DCompleter::complete(const QRect& rect) +{ + QModelIndex idx = proxy->currentIndex(false); + hiddenBecauseNoMatch = false; + if (mode == QCompleter::InlineCompletion) { + if (idx.isValid()) + completed(idx, true); + return; + } + + Q_ASSERT(widgetptr != nullptr); + if ((mode == QCompleter::PopupCompletion && !idx.isValid()) + || (mode == QCompleter::UnfilteredPopupCompletion && proxy->rowCount() == 0)) { + if (popupA) + popupA->hide(); // no suggestion, hide + hiddenBecauseNoMatch = true; + return; + } + + popup(); + if (mode == QCompleter::UnfilteredPopupCompletion) + setCurrentIndex(idx, false); + + showPopup(rect); + popupRect = rect; +} + +/*! + Sets the current row to the \a row specified. Returns \c true if successful; + otherwise returns \c false. + + This function may be used along with currentCompletion() to iterate + through all the possible completions. + + \sa currentCompletion(), completionCount() +*/ +bool DCompleter::setCurrentRow(int row) +{ + return proxy->setCurrentRow(row); +} + +/*! + Returns the current row. + + \sa setCurrentRow() +*/ +int DCompleter::currentRow() const +{ + return proxy->currentRow(); +} + +/*! + Returns the number of completions for the current prefix. For an unsorted + model with a large number of items this can be expensive. Use setCurrentRow() + and currentCompletion() to iterate through all the completions. +*/ +int DCompleter::completionCount() const +{ + return proxy->completionCount(); +} + +/*! + \enum DCompleter::ModelSorting + + This enum specifies how the items in the model are sorted. + + \value UnsortedModel The model is unsorted. + \value CaseSensitivelySortedModel The model is sorted case sensitively. + \value CaseInsensitivelySortedModel The model is sorted case insensitively. + + \sa setModelSorting() +*/ + +/*! + \property DCompleter::modelSorting + \brief the way the model is sorted + + By default, no assumptions are made about the order of the items + in the model that provides the completions. + + If the model's data for the completionColumn() and completionRole() is sorted in + ascending order, you can set this property to \l CaseSensitivelySortedModel + or \l CaseInsensitivelySortedModel. On large models, this can lead to + significant performance improvements because the completer object can + then use a binary search algorithm instead of linear search algorithm. + + The sort order (i.e ascending or descending order) of the model is determined + dynamically by inspecting the contents of the model. + + \b{Note:} The performance improvements described above cannot take place + when the completer's \l caseSensitivity is different to the case sensitivity + used by the model's when sorting. + + \sa setCaseSensitivity(), DCompleter::ModelSorting +*/ +void DCompleter::setModelSorting(QCompleter::ModelSorting sorting) +{ + if (this->sorting == sorting) + return; + this->sorting = sorting; + proxy->createEngine(); + proxy->invalidate(); +} + +QCompleter::ModelSorting DCompleter::modelSorting() const +{ + return sorting; +} + +/*! + \property DCompleter::completionColumn + \brief the column in the model in which completions are searched for. + + If the popup() is a QListView, it is automatically setup to display + this column. + + By default, the match column is 0. + + \sa completionRole, caseSensitivity +*/ +void DCompleter::setCompletionColumn(int column) +{ + if (this->column == column) + return; +#if QT_CONFIG(listview) + if (QListView *listView = qobject_cast(popupA)) + listView->setModelColumn(column); +#endif + this->column = column; + proxy->invalidate(); +} + +int DCompleter::completionColumn() const +{ + return column; +} + +/*! + \property DCompleter::completionRole + \brief the item role to be used to query the contents of items for matching. + + The default role is Qt::EditRole. + + \sa completionColumn, caseSensitivity +*/ +void DCompleter::setCompletionRole(int role) +{ + if (this->role == role) + return; + this->role = role; + this->proxy->invalidate(); +} + +int DCompleter::completionRole() const +{ + return role; +} + +/*! + \property DCompleter::wrapAround + \brief the completions wrap around when navigating through items + \since 4.3 + + The default is true. +*/ +void DCompleter::setWrapAround(bool wrap) +{ + if (this->wrap == wrap) + return; + this->wrap = wrap; +} + +bool DCompleter::wrapAround() const +{ + return wrap; +} + +/*! + \property DCompleter::maxVisibleItems + \brief the maximum allowed size on screen of the completer, measured in items + \since 4.6 + + By default, this property has a value of 7. +*/ +int DCompleter::maxVisibleItems() const +{ + return maxVisibleItemsCount; +} + +void DCompleter::setMaxVisibleItems(int maxItems) +{ + if (Q_UNLIKELY(maxItems < 0)) { + qWarning("DCompleter::setMaxVisibleItems: " + "Invalid max visible items (%d) must be >= 0", maxItems); + return; + } + maxVisibleItemsCount = maxItems; +} + +/*! + \property DCompleter::caseSensitivity + \brief the case sensitivity of the matching + + The default is Qt::CaseSensitive. + + \sa completionColumn, completionRole, modelSorting +*/ +void DCompleter::setCaseSensitivity(Qt::CaseSensitivity cs) +{ + if (this->cs == cs) + return; + this->cs = cs; + proxy->createEngine(); + proxy->invalidate(); +} + +Qt::CaseSensitivity DCompleter::caseSensitivity() const +{ + return cs; +} + +/*! + \property DCompleter::completionPrefix + \brief the completion prefix used to provide completions. + + The completionModel() is updated to reflect the list of possible + matches for \a prefix. +*/ +void DCompleter::setCompletionPrefix(const QString &prefix) +{ + this->prefix = prefix; + proxy->filter(splitPath(prefix)); +} + +QString DCompleter::completionPrefix() const +{ + return prefix; +} + +/*! + Returns the model index of the current completion in the completionModel(). + + \sa setCurrentRow(), currentCompletion(), model() +*/ +QModelIndex DCompleter::currentIndex() const +{ + return proxy->currentIndex(false); +} + +/*! + Returns the current completion string. This includes the \l completionPrefix. + When used alongside setCurrentRow(), it can be used to iterate through + all the matches. + + \sa setCurrentRow(), currentIndex() +*/ +QString DCompleter::currentCompletion() const +{ + return pathFromIndex(proxy->currentIndex(true)); +} + +/*! + Returns the completion model. The completion model is a read-only list model + that contains all the possible matches for the current completion prefix. + The completion model is auto-updated to reflect the current completions. + + \note The return value of this function is defined to be an QAbstractItemModel + purely for generality. This actual kind of model returned is an instance of an + QAbstractProxyModel subclass. + + \sa completionPrefix, model() +*/ +QAbstractItemModel *DCompleter::completionModel() const +{ + return proxy; +} + +/*! + Returns the path for the given \a index. The completer object uses this to + obtain the completion text from the underlying model. + + The default implementation returns the \l{Qt::EditRole}{edit role} of the + item for list models. It returns the absolute file path if the model is a + QFileSystemModel. + + \sa splitPath() +*/ + +QString DCompleter::pathFromIndex(const QModelIndex& index) const +{ + if (!index.isValid()) + return QString(); + + QAbstractItemModel *sourceModel = proxy->sourceModel(); + if (!sourceModel) + return QString(); + bool isDirModel = false; + bool isFsModel = false; +#if QT_CONFIG(dirmodel) + isDirModel = qobject_cast(proxy->sourceModel()) != nullptr; +#endif +#if QT_CONFIG(filesystemmodel) + isFsModel = qobject_cast(proxy->sourceModel()) != nullptr; +#endif + if (!isDirModel && !isFsModel) + return sourceModel->data(index, role).toString(); + + QModelIndex idx = index; + QStringList list; + do { + QString t; + if (isDirModel) + t = sourceModel->data(idx, Qt::EditRole).toString(); +#if QT_CONFIG(filesystemmodel) + else + t = sourceModel->data(idx, QFileSystemModel::FileNameRole).toString(); +#endif + list.prepend(t); + QModelIndex parent = idx.parent(); + idx = parent.sibling(parent.row(), index.column()); + } while (idx.isValid()); + +#if !defined(Q_OS_WIN) + if (list.count() == 1) // only the separator or some other text + return list[0]; + list[0].clear() ; // the join below will provide the separator +#endif + + return list.join(QDir::separator()); +} + +/*! + Splits the given \a path into strings that are used to match at each level + in the model(). + + The default implementation of splitPath() splits a file system path based on + QDir::separator() when the sourceModel() is a QFileSystemModel. + + When used with list models, the first item in the returned list is used for + matching. + + \sa pathFromIndex(), {Handling Tree Models} +*/ +QStringList DCompleter::splitPath(const QString& path) const +{ + bool isDirModel = false; + bool isFsModel = false; +#if QT_CONFIG(dirmodel) + isDirModel = qobject_cast(proxy->sourceModel()) != nullptr; +#endif +#if QT_CONFIG(filesystemmodel) + isFsModel = qobject_cast(proxy->sourceModel()) != nullptr; +#endif + + if ((!isDirModel && !isFsModel) || path.isEmpty()) + return QStringList(completionPrefix()); + + QString pathCopy = QDir::toNativeSeparators(path); +#if defined(Q_OS_WIN) + if (pathCopy == QLatin1String("\\") || pathCopy == QLatin1String("\\\\")) + return QStringList(pathCopy); + const bool startsWithDoubleSlash = pathCopy.startsWith(QLatin1String("\\\\")); + if (startsWithDoubleSlash) + pathCopy = pathCopy.mid(2); +#endif + + const QChar sep = QDir::separator(); + QStringList parts = pathCopy.split(sep); + +#if defined(Q_OS_WIN) + if (startsWithDoubleSlash) + parts[0].prepend(QLatin1String("\\\\")); +#else + if (pathCopy[0] == sep) // readd the "/" at the beginning as the split removed it + parts[0] = QLatin1Char('/'); +#endif + + return parts; +} + +void DCompleter::init(QAbstractItemModel *m) +{ + proxy = new DCompletionModel(this, this); + QObject::connect(proxy, &DCompletionModel::rowsAdded, this, &DCompleter::onAutoResizePopup); + setModel(m); +#if !QT_CONFIG(listview) + setCompletionMode(QCompleter::InlineCompletion); +#else + setCompletionMode(QCompleter::PopupCompletion); +#endif // QT_CONFIG(listview) +} + +void DCompleter::setCurrentIndex(QModelIndex index, bool select) +{ + if (!popup()) + return; + if (!select) { + popupA->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } else { + if (!index.isValid()) + popupA->selectionModel()->clear(); + else + popupA->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select + | QItemSelectionModel::Rows); + } + index = popupA->selectionModel()->currentIndex(); + if (!index.isValid()) + popupA->scrollToTop(); + else + popupA->scrollTo(index, QAbstractItemView::PositionAtTop); +} + +void DCompleter::completionSelected(const QItemSelection& selection) +{ + QModelIndex index; + if (!selection.indexes().isEmpty()) + index = selection.indexes().first(); + + completed(index, true); +} + +void DCompleter::completed(QModelIndex index, bool highlight) +{ + QString completion; + + if (!index.isValid() || (!proxy->showAll && (index.row() >= proxy->engine->matchCount()))) { + completion = prefix; + index = QModelIndex(); + } else { + if (!(index.flags() & Qt::ItemIsEnabled)) + return; + QModelIndex si = proxy->mapToSource(index); + si = si.sibling(si.row(), column); // for clicked() + completion = pathFromIndex(si); +#if QT_CONFIG(dirmodel) + // add a trailing separator in inline + if (mode == QCompleter::InlineCompletion) { + if (qobject_cast(proxy->sourceModel()) && QFileInfo(completion).isDir()) + completion += QDir::separator(); + } +#endif +#if QT_CONFIG(filesystemmodel) + // add a trailing separator in inline + if (mode == QCompleter::InlineCompletion) { + if (qobject_cast(proxy->sourceModel()) && QFileInfo(completion).isDir()) + completion += QDir::separator(); + } +#endif + } + + if (highlight) { + emit highlighted(index); + emit highlighted(completion); + } else { + emit activated(index); + emit activated(completion); + } +} + +void DCompleter::autoResizePopup() +{ + if (!popupA || !popupA->isVisible()) + return; + showPopup(popupRect); +} + +void DCompleter::showPopup(const QRect& rect) +{ + const QRect screen = QApplication::desktop()->availableGeometry(widgetptr.data()); + Qt::LayoutDirection dir = widgetptr->layoutDirection(); + QPoint pos; + int rh, w; + int h = (popupA->sizeHintForRow(0) * qMin(maxVisibleItemsCount, popupA->model()->rowCount()) + 3) + 3; + QScrollBar *hsb = popupA->horizontalScrollBar(); + if (hsb && hsb->isVisible()) + h += popupA->horizontalScrollBar()->sizeHint().height(); + + if (rect.isValid()) { + rh = rect.height(); + w = rect.width(); + pos = widgetptr->mapToGlobal(dir == Qt::RightToLeft ? rect.bottomRight() : rect.bottomLeft()); + } else { + rh = widgetptr->height(); + pos = widgetptr->mapToGlobal(QPoint(0, widgetptr->height() - 2)); + w = widgetptr->width(); + } + + if (w > screen.width()) + w = screen.width(); + if ((pos.x() + w) > (screen.x() + screen.width())) + pos.setX(screen.x() + screen.width() - w); + if (pos.x() < screen.x()) + pos.setX(screen.x()); + + int top = pos.y() - rh - screen.top() + 2; + int bottom = screen.bottom() - pos.y(); + h = qMax(h, popupA->minimumHeight()); + if (h > bottom) { + h = qMin(qMax(top, bottom), h); + + if (top > bottom) + pos.setY(pos.y() - h - rh + 2); + } + + popupA->setGeometry(pos.x(), pos.y(), w, h); + + if (!popupA->isVisible()) + popupA->show(); +} + +void DCompleter::fileSystemModelDirectoryLoaded(const QString &path) +{ + // Slot called when QFileSystemModel has finished loading. + // If we hide the popup because there was no match because the model was not loaded yet, + // we re-start the completion when we get the results + if (hiddenBecauseNoMatch + && prefix.startsWith(path) && prefix != (path + QLatin1Char('/')) + && widgetptr) { + complete(); + } +} diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.h b/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.h new file mode 100644 index 0000000000..ffd0af60a3 --- /dev/null +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/dcompleter.h @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: 2021 - 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DCOMPLETER_H +#define DCOMPLETER_H + +#include + +#include +#include +#include +#include +#include +#include + +class DCompleter; +class DIndexMapper +{ +public: + DIndexMapper() : v(false), f(0), t(-1) { } + DIndexMapper(int f, int t) : v(false), f(f), t(t) { } + DIndexMapper(const QVector &vec) : v(true), vector(vec), f(-1), t(-1) { } + + inline int count() const { return v ? vector.count() : t - f + 1; } + inline int operator[] (int index) const { return v ? vector[index] : f + index; } + inline int indexOf(int x) const { return v ? vector.indexOf(x) : ((t < f) ? -1 : x - f); } + inline bool isValid() const { return !isEmpty(); } + inline bool isEmpty() const { return v ? vector.isEmpty() : (t < f); } + inline void append(int x) { Q_ASSERT(v); vector.append(x); } + inline int first() const { return v ? vector.first() : f; } + inline int last() const { return v ? vector.last() : t; } + inline int from() const { Q_ASSERT(!v); return f; } + inline int to() const { Q_ASSERT(!v); return t; } + inline int cost() const { return vector.count()+2; } + +private: + bool v; + QVector vector; + int f, t; +}; + +struct DMatchData { + DMatchData() : exactMatchIndex(-1), partial(false) { } + DMatchData(const DIndexMapper& indices, int em, bool p) : + indices(indices), exactMatchIndex(em), partial(p) { } + DIndexMapper indices; + inline bool isValid() const { return indices.isValid(); } + int exactMatchIndex; + bool partial; +}; + +class DCompletionEngine +{ +public: + typedef QMap CacheItem; + typedef QMap Cache; + + DCompletionEngine(DCompleter *c) : c(c), curRow(-1), cost(0) { } + virtual ~DCompletionEngine(); + + void filter(const QStringList &parts); + + DMatchData filterHistory(); + bool matchHint(const QString &part, const QModelIndex &parent, DMatchData *m) const; + + void saveInCache(QString, const QModelIndex&, const DMatchData&); + bool lookupCache(const QString &part, const QModelIndex &parent, DMatchData *m) const; + + virtual void filterOnDemand(int) { } + virtual DMatchData filter(const QString&, const QModelIndex&, int) = 0; + + int matchCount() const { return curMatch.indices.count() + historyMatch.indices.count(); } + + DMatchData curMatch, historyMatch; + DCompleter *c; + QStringList curParts; + QModelIndex curParent; + int curRow; + + Cache cache; + int cost; +}; + +class DSortedModelEngine : public DCompletionEngine +{ +public: + DSortedModelEngine(DCompleter *c) : DCompletionEngine(c) { } + DMatchData filter(const QString&, const QModelIndex&, int) override; + DIndexMapper indexHint(QString, const QModelIndex&, Qt::SortOrder); + Qt::SortOrder sortOrder(const QModelIndex&) const; +}; + +class DUnsortedModelEngine : public DCompletionEngine +{ +public: + DUnsortedModelEngine(DCompleter *c) : DCompletionEngine(c) { } + + void filterOnDemand(int) override; + DMatchData filter(const QString&, const QModelIndex&, int) override; +private: + int buildIndices(const QString& str, const QModelIndex& parent, int n, + const DIndexMapper& iv, DMatchData* m); +}; + +class DCompleterItemDelegate : public QItemDelegate +{ +public: + DCompleterItemDelegate(QAbstractItemView *view) + : QItemDelegate(view), view(view) { } + void paint(QPainter *p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override; + +private: + QAbstractItemView *view{ nullptr }; +}; + +class DCompletionModel : public QAbstractProxyModel +{ + Q_OBJECT +public: + DCompletionModel(DCompleter *c, QObject *parent); + + void createEngine(); + void setFiltered(bool); + void filter(const QStringList& parts); + int completionCount() const; + int currentRow() const { return engine->curRow; } + bool setCurrentRow(int row); + QModelIndex currentIndex(bool) const; + + QModelIndex index(int row, int column, const QModelIndex & = QModelIndex()) const override; + int rowCount(const QModelIndex &index = QModelIndex()) const override; + int columnCount(const QModelIndex &index = QModelIndex()) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex & = QModelIndex()) const override { return QModelIndex(); } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + void setSourceModel(QAbstractItemModel *sourceModel) override; + QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; + QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; + + DCompleter *c{ nullptr }; + QScopedPointer engine{ nullptr }; + bool showAll{ false }; + +signals: + void rowsAdded(); + +public Q_SLOTS: + void invalidate(); + void rowsInserted(); + void modelDestroyed(); +}; + +class DCompleter : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString completionPrefix READ completionPrefix WRITE setCompletionPrefix) + Q_PROPERTY(QCompleter::ModelSorting modelSorting READ modelSorting WRITE setModelSorting) + Q_PROPERTY(Qt::MatchFlags filterMode READ filterMode WRITE setFilterMode) + Q_PROPERTY(QCompleter::CompletionMode completionMode READ completionMode WRITE setCompletionMode) + Q_PROPERTY(int completionColumn READ completionColumn WRITE setCompletionColumn) + Q_PROPERTY(int completionRole READ completionRole WRITE setCompletionRole) + Q_PROPERTY(int maxVisibleItems READ maxVisibleItems WRITE setMaxVisibleItems) + Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity) + Q_PROPERTY(bool wrapAround READ wrapAround WRITE setWrapAround) + +public: + DCompleter(QObject *parent = nullptr); + DCompleter(QAbstractItemModel *model, QObject *parent = nullptr); +#ifndef QT_NO_STRINGLISTMODEL + DCompleter(const QStringList& completions, QObject *parent = nullptr); +#endif + ~DCompleter() override; + + void setWidget(QWidget *widget); + QWidget *widget() const; + + void setModel(QAbstractItemModel *c); + QAbstractItemModel *model() const; + + void setCompletionMode(QCompleter::CompletionMode mode); + QCompleter::CompletionMode completionMode() const; + + void setFilterMode(Qt::MatchFlags filterMode); + Qt::MatchFlags filterMode() const; + + QAbstractItemView *popup() const; + void setPopup(QAbstractItemView *popup); + + void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); + Qt::CaseSensitivity caseSensitivity() const; + + void setModelSorting(QCompleter::ModelSorting sorting); + QCompleter::ModelSorting modelSorting() const; + + void setCompletionColumn(int column); + int completionColumn() const; + + void setCompletionRole(int role); + int completionRole() const; + + bool wrapAround() const; + + int maxVisibleItems() const; + void setMaxVisibleItems(int maxItems); + + int completionCount() const; + bool setCurrentRow(int row); + int currentRow() const; + + QModelIndex currentIndex() const; + QString currentCompletion() const; + + QAbstractItemModel *completionModel() const; + + QString completionPrefix() const; + +public Q_SLOTS: + void setCompletionPrefix(const QString &prefix); + void complete(const QRect& rect = QRect()); + void setWrapAround(bool wrap); + +public: + virtual QString pathFromIndex(const QModelIndex &index) const; + virtual QStringList splitPath(const QString &path) const; + +protected: + bool eventFilter(QObject *o, QEvent *e) override; + bool event(QEvent *) override; + +private Q_SLOTS: + void onComplete(QModelIndex index); + void onCompletionSelected(const QItemSelection& s); + void onAutoResizePopup(); + void onFileSystemModelDirectoryLoaded(const QString& st); + +Q_SIGNALS: + void activated(const QString &text); + void activated(const QModelIndex &index); + void highlighted(const QString &text); + void highlighted(const QModelIndex &index); + +public: + void init(QAbstractItemModel *model = nullptr); + + QPointer widgetptr{ nullptr }; + DCompletionModel *proxy{ nullptr }; + QAbstractItemView *popupA{ nullptr }; + QCompleter::CompletionMode mode; + Qt::MatchFlags filterModes; + + QString prefix; + Qt::CaseSensitivity cs; + int role{ 0 }; + int column{ 0 }; + int maxVisibleItemsCount{ 0 }; + QCompleter::ModelSorting sorting; + bool wrap{ false }; + + bool eatFocusOut{ false }; + QRect popupRect; + bool hiddenBecauseNoMatch{ false }; + + void showPopup(const QRect&); + void completed(QModelIndex, bool highlight = false); + void completionSelected(const QItemSelection&); + void autoResizePopup(); + void fileSystemModelDirectoryLoaded(const QString &path); + void setCurrentIndex(QModelIndex, bool = true); + +private: + Q_DISABLE_COPY(DCompleter) +}; + +#endif // DCOMPLETER_H diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/private/addressbar_p.h b/src/plugins/filemanager/core/dfmplugin-titlebar/views/private/addressbar_p.h index f6455529a9..6030dec73c 100644 --- a/src/plugins/filemanager/core/dfmplugin-titlebar/views/private/addressbar_p.h +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/private/addressbar_p.h @@ -10,8 +10,10 @@ #include "views/completerview.h" #include "views/completerviewdelegate.h" #include "models/completerviewmodel.h" +#include "views/dcompleter.h" #include +#include #include #include @@ -59,7 +61,7 @@ class AddressBarPrivate : public QObject CrumbInterface *crumbController { nullptr }; CompleterViewModel completerModel; CompleterView *completerView { nullptr }; - QCompleter *urlCompleter { nullptr }; + DCompleter *urlCompleter { nullptr }; CompleterViewDelegate *cpItemDelegate { nullptr }; // inputMethodEvent中获取不到选中的内容,故缓存光标开始位置以及选中长度 int selectPosStart { 0 }; @@ -68,6 +70,7 @@ class AddressBarPrivate : public QObject QRegExp protocolIPRegExp; // smb://ip, ftp://ip, sftp://ip bool inputIsIpAddress { false }; bool isKeepVisible { false }; + bool isClearSearch { false }; public: explicit AddressBarPrivate(AddressBar *qq); @@ -77,7 +80,7 @@ class AddressBarPrivate : public QObject void initData(); void updateHistory(); void setIndicator(enum AddressBar::IndicatorType type); - void setCompleter(QCompleter *c); + void setCompleter(DCompleter *c); void clearCompleterModel(); void updateCompletionState(const QString &text); void doComplete(); @@ -99,6 +102,8 @@ public Q_SLOTS: void appendToCompleterModel(const QStringList &stringList); void onTravelCompletionListFinished(); void onIndicatorTriggerd(); + void onGenericAttributeChanged(Application::GenericAttribute ga, const QVariant &value); + protected: virtual bool eventFilterResize(AddressBar *addressbar, QResizeEvent *event); diff --git a/src/plugins/filemanager/core/dfmplugin-titlebar/views/titlebarwidget.cpp b/src/plugins/filemanager/core/dfmplugin-titlebar/views/titlebarwidget.cpp index c574151beb..e7f96b4750 100644 --- a/src/plugins/filemanager/core/dfmplugin-titlebar/views/titlebarwidget.cpp +++ b/src/plugins/filemanager/core/dfmplugin-titlebar/views/titlebarwidget.cpp @@ -162,6 +162,7 @@ void TitleBarWidget::initConnect() connect(addressBar, &AddressBar::escKeyPressed, this, [this]() { if (crumbBar->controller()) crumbBar->controller()->processAction(CrumbInterface::kEscKeyPressed); + addressBar->stopSpinner(); }); connect(addressBar, &AddressBar::lostFocus, this, [this]() { if (crumbBar->controller()) diff --git a/tests/plugins/filemanager/core/dfmplugin-workspace/views/ut_fileview.cpp b/tests/plugins/filemanager/core/dfmplugin-workspace/views/ut_fileview.cpp index d10c1ea9f0..034f6f8e91 100644 --- a/tests/plugins/filemanager/core/dfmplugin-workspace/views/ut_fileview.cpp +++ b/tests/plugins/filemanager/core/dfmplugin-workspace/views/ut_fileview.cpp @@ -27,7 +27,7 @@ class UT_FileView : public testing::Test TEST_F(UT_FileView, setViewSelectState) { - stub.set_lamda(&Application::appAttribute, [](Application::ApplicationAttribute)->QVariant { + stub.set_lamda(&Application::appAttribute, [](Application::ApplicationAttribute, const QVariant&)->QVariant { return QVariant(); }); stub.set_lamda(&FileView::initializeModel, [](){}); @@ -39,7 +39,7 @@ TEST_F(UT_FileView, setViewSelectState) TEST_F(UT_FileView, paintEvent) { - stub.set_lamda(&Application::appAttribute, [](Application::ApplicationAttribute)->QVariant { + stub.set_lamda(&Application::appAttribute, [](Application::ApplicationAttribute, const QVariant&)->QVariant { return QVariant(); }); stub.set_lamda(&FileView::initializeModel, [](){}); diff --git a/translations/dde-file-manager.ts b/translations/dde-file-manager.ts index cc5aad039e..714c4c8af9 100644 --- a/translations/dde-file-manager.ts +++ b/translations/dde-file-manager.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed The device has been safely removed - + eject eject - + unmount Unmount - - + + remove remove - + Operation failed Operation failed - + Device (%1) is busy, cannot %2 now. Device (%1) is busy, cannot %2 now. @@ -1716,6 +1716,19 @@ or + + + + + + + + + + + Clear search history + + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button OK @@ -2549,8 +2563,9 @@ - - + + + Cancel button Cancel @@ -2579,7 +2594,7 @@ - + Run button Run @@ -2723,22 +2738,27 @@ Unable to access %1 - + + Are you sure clear search histories? + + + + %1 that this shortcut refers to has been changed or moved %1 that this shortcut refers to has been changed or moved - + Do you want to delete this shortcut? Do you want to delete this shortcut? - + This file is not executable, do you want to add the execute permission and run? This file is not executable, do you want to add the execute permission and run? - + The selected files contain system file/directory, and it cannot be deleted The selected files contain system file/directory, and it cannot be deleted @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window Always open folder in new window - + Open file: Open file: - + Click Click - + Double click Double click - + New window and tab New window and tab - + Open from default window: Open from default window: - - + + Computer Computer - - + + Home Home - - + + Desktop Desktop - - + + Videos Videos - - + + Music Music - - + + Pictures Pictures - - + + Documents Documents - - + + Downloads Downloads - + Open in new tab: Open in new tab: - + Current Directory Current Directory - + Files and folders Files and folders - + Show hidden files Show hidden files - + Show file extensions Show file extensions - + Mix sorting of files and folders Mix sorting of files and folders - + + Others + Others + + + + Display search history + + + + Workspace Workspace - + View View - + Default size: Default size: - + Extra small Extra small - + Small Small - + Medium Medium - + Large Large - + Extra large Extra large - + Tree - + Default view: Default view: - + Icon Icon - + List List - + Restore default view mode for all directories Restore default view mode for all directories - + Restore default view mode Restore default view mode - + Thumbnail preview Thumbnail preview - + Compressed file preview Compressed file preview - + Text preview Text preview - + Document preview Document preview - + Image preview Image preview - + Video preview Video preview - + Music preview Music preview - + The remote environment shows thumbnail previews The remote environment shows thumbnail previews - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze - + Advanced Advanced - + Mount Mount - + Auto mount Auto mount - + Open after auto mount Open after auto mount - + Merge the entries of Samba shared folders Merge the entries of Samba shared folders - + Switching the entry display may lead to failed mounting Switching the entry display may lead to failed mounting - + Use the file chooser dialog of File Manager Use the file chooser dialog of File Manager - + Ask for my confirmation when deleting files Ask for my confirmation when deleting files @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address Search or enter address @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search - + advanced search diff --git a/translations/dde-file-manager_bo.ts b/translations/dde-file-manager_bo.ts index 865487700e..69273a6785 100644 --- a/translations/dde-file-manager_bo.ts +++ b/translations/dde-file-manager_bo.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed སྒྲིག་ཆས་བདེ་འཇགས་ངང་སྤོ་འབུད་བྱས་སོང་། - + eject ཕྱིར་འདོན། - + unmount བཤིག་འདོན། - - + + remove སུབ་པ། - + Operation failed བཀོལ་སྤྱོད་བྱེད་མ་ཐུབ། - + Device (%1) is busy, cannot %2 now. སྒྲིག་(%1)སྤྱོད་བཞིན་པས། %2བྱེད་ཐབས་མེད། @@ -1716,6 +1716,19 @@ or ཡང་ན། + + + + + + + + + + + Clear search history + + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button ཆོག @@ -2549,8 +2563,9 @@ - - + + + Cancel button འདོར་བ། @@ -2579,7 +2594,7 @@ - + Run button འཁོར་སྐྱོད། @@ -2723,22 +2738,27 @@ ལྟ་སྤྱོད་%1བྱེད་མ་ཐུབ། - + + Are you sure clear search histories? + + + + %1 that this shortcut refers to has been changed or moved མྱུར་ལམ་འདིས་སྟོན་པའི་“%1”སྒྱུར་ཟིན་པའམ་སྤོས་འདུག - + Do you want to delete this shortcut? མྱུར་ལམ་འདི་བསུབ་དགོས་སམ། - + This file is not executable, do you want to add the execute permission and run? ཡིག་ཆ་འདིར་ལག་བསྟར་བྱེད་དབང་མེད་པས། དབང་ཚད་སྣོན་ཏེ་འཁོར་སྐྱོད་བྱེད་དགོས་སམ། - + The selected files contain system file/directory, and it cannot be deleted བདམས་ཟིན་པའི་ཡིག་ཆའི་ནང་རྒྱུད་ཁོངས་ཀྱི་དཀར་ཆག་ཚུད་ཡོད་པས། སུབ་ཐབས་མི་འདུག @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window རྟག་ཏུ་སྒེའུ་ཁུང་གསར་པའི་ནང་ཡིག་ཁུག་ཁ་འབྱེད་པ། - + Open file: ཡིག་ཆ་ཁ་ཕྱེ། - + Click གཅིག་རྡེབ། - + Double click ཉིས་རྡེབ། - + New window and tab སྒེའུ་ཁུང་གསར་པ་དང་ཤོག་བྱང་གསར་པ། - + Open from default window: སོར་བཞག་སྒེའུ་ཁུང་ནས་ཁ་འབྱེད་པ། - - + + Computer རྩིས་འཁོར། - - + + Home དཀར་ཆག་གཙོ་བོ། - - + + Desktop ཅོག་ངོས། - - + + Videos བརྙན་འཕྲིན། - - + + Music རོལ་མོ། - - + + Pictures པར་རིས། - - + + Documents ཡིག་ཆ། - - + + Downloads ཕབ་ལེན། - + Open in new tab: ཤོག་བྱང་གསར་པ་ནས་ཁ་ཕྱེ། - + Current Directory མིག་སྔའི་དཀར་ཆག - + Files and folders ཡིག་ཆ་དང་དཀར་ཆག - + Show hidden files ཡིབ་པའི་ཡིག་ཆ་མངོན་སྟོན། - + Show file extensions ཡིག་ཆའི་རྒྱ་བསྐྱེད་མིང་མངོན་པ། - + Mix sorting of files and folders ཡིག་ཆ་དང་ཡིག་ཁུག་གོ་རིམ་དཀྲུགས་ནས་སྒྲིག་པ། - + + Others + གཞན། + + + + Display search history + + + + Workspace ལས་ཁུལ། - + View མཐོང་རིས། - + Default size: སོར་བཞག་ཆེ་ཆུང་། - + Extra small ཆུང་གྲས། - + Small ཆུང་། - + Medium འབྲིང་། - + Large ཆེ། - + Extra large ཆེ་གྲས། - + Tree སྡོང་དབྱིབས་མཐོང་རིས། - + Default view: སོར་བཞག་མཐོང་རིས། - + Icon རྟགས་རིས་མཐོང་རིས། - + List གསལ་ཐོའི་མཐོང་རིས། - + Restore default view mode for all directories དཀར་ཆག་ཡོད་ཚད་ཀྱི་མཐོང་རིས་སོར་བཞག - + Restore default view mode སོར་བཞག་མཐོང་རིས་སླར་དུ་གསོ་བ། - + Thumbnail preview བསྡུས་རིས་སྔོན་ལྟ། - + Compressed file preview སྡུད་སྒྲིལ་ཡིག་ཆར་སྔོན་ལྟ། - + Text preview ཡིག་ཆ་སྔོན་ལྟ། - + Document preview ཡིག་ཆ་སྔོན་ལྟ། - + Image preview པར་རིས་སྔོན་ལྟ། - + Video preview བརྙན་འཕྲིན་སྔོན་ལྟ། - + Music preview གླུ་དབྱངས་སྔོན་ལྟ། - + The remote environment shows thumbnail previews རྒྱང་སྦྲེལ་ཁོར་ཡུག་ནས་བསྡུས་རིས་ལ་སྔོན་ལྟ་བྱེད་པ། - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze བསྡུས་རིས་སྔོན་ལྟ་ཞེས་པ་ཁ་ཕྱེ་ཚེ་རྒྱང་སྦྲེལ་དཀར་ཆག་སྣོན་འཇུག་བྱེད་དལ་དུ་འགྲོ་བ་དང་ཡང་ན་འགག་འགྲོ་སྲིད། - + Advanced མཐོ་རིམ་སྒྲིག་འགོད། - + Mount འཇུག་པ། - + Auto mount རང་འཇུག - + Open after auto mount རང་འཇུག་བྱས་རྗེས་ཁ་ཕྱེ། - + Merge the entries of Samba shared folders ཟླ་བསྒྲིལ་ནས་Sambaམཉམ་སྤྱོད་དཀར་ཆག་གི་འཛུལ་སྒོ་མངོན་པ། - + Switching the entry display may lead to failed mounting འཛུལ་སྒོའི་འཆར་སྟོན་དཔེ་རྣམ་བརྗེས་ཚེ་འགེལ་འཇུག་གི་རྣམ་པ་ཕམ་སྲིད། - + Use the file chooser dialog of File Manager ཡིག་ཆ་དོ་དམ་ཆས་ཀྱི་ཡིག་ཆ་བེད་སྤྱོད་བྱས་ཏེ་གླེང་སྒྲོམ་འདེམས་པ། - + Ask for my confirmation when deleting files སྤྱིར་བཏང་གི་བསུབ་རྒྱུའི་དྲན་སྐུལ་ཁ་ཕྱེ་བ། @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address བཤེར་འཚོལ་ལམ་གནས་ཡུལ་ནང་འཇུག་བྱེད་པ། @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search བཤེར་འཚོལ། - + advanced search མཐོ་རིམ་བཤེར་འཚོལ། diff --git a/translations/dde-file-manager_ug.ts b/translations/dde-file-manager_ug.ts index d12ef8a6a1..0feaa2d454 100644 --- a/translations/dde-file-manager_ug.ts +++ b/translations/dde-file-manager_ug.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed ئۈسكۈنە بىخەتەر چىقىرىۋىتىلدى - + eject چىقىرۋېتىش - + unmount ئۆچۈرۈش - - + + remove چىقىرۋېتىش - + Operation failed مەشغۇلات مەغلۇب بولدى - + Device (%1) is busy, cannot %2 now. ئۈسكۈنە (%1)  ئالدىراش، %2 قا ئامالسىز. @@ -1716,6 +1716,19 @@ or ياكى + + + + + + + + + + + Clear search history + + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button ماقۇل @@ -2549,8 +2563,9 @@ - - + + + Cancel button بىكار قىلىش @@ -2579,7 +2594,7 @@ - + Run button يۈرگۈزۈش @@ -2723,22 +2738,27 @@ %1نى زىيارەت قىلغىلى بولمىدى - + + Are you sure clear search histories? + + + + %1 that this shortcut refers to has been changed or moved بۇ تېزلەتمە كۆرسەتكەن “%1” ئۆزگەرتىۋېتىلگەن ياكى يۆتكىۋېتىلگەن - + Do you want to delete this shortcut? بۇ تېزلەتمىنى ئۆچۈرەمسىز؟ - + This file is not executable, do you want to add the execute permission and run? بۇ ھۆججەتنىڭ ئىجرا قىلىشقا بولىدىغان ھوقۇق يوق. ھوقۇق قوشۇپ ئىجرا قىلماقچىمۇ؟ - + The selected files contain system file/directory, and it cannot be deleted تاللانغان ھۆججەتتە سىستېما مۇندەرىجىسى بار، ئۆچۈرگىلى بولمايدۇ @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window ھۆججەت قىسقۇچنى دائىم يىڭى كۆزنەكتە ئېچىش - + Open file: ھۆججەت ئېچىش: - + Click تاق چېكىش - + Double click قوش چېكىش - + New window and tab يېڭى كۆزنەك ۋە يېڭى بەتكۈچ - + Open from default window: سۈكۈتتىكى كۆزنەكتە ئېچىش: - - + + Computer كومپيوتېر - - + + Home باش مۇندەرىجە - - + + Desktop ئۈستەليۈزى - - + + Videos سىن - - + + Music مۇزىكا - - + + Pictures رەسىم - - + + Documents ھۆججەت - - + + Downloads چۈشۈرۈلمىلەر - + Open in new tab: يىڭى بەتكۈچتە ئېچىش: - + Current Directory نۆۋەتتىكى مۇندەرىجە - + Files and folders ھۆججەت ۋە مۇندەرىجە - + Show hidden files يوشۇرۇن ھۆججەتلەر كۆرۈنسۇن - + Show file extensions كېڭەيتىلگەن نامى كۆرۈنسۇن - + Mix sorting of files and folders ھۆججەت بىلەن ھۆججەت قىسقۇچنى ئارىلاش تىزىش - + + Others + باشقىلىرى + + + + Display search history + + + + Workspace خىزمەت رايونى - + View كۆرۈنۈش - + Default size: سۈكۈتتىكى چوڭلۇقى: - + Extra small بەك كىچىك - + Small كىچىك - + Medium ئوتتۇرا - + Large چوڭ - + Extra large بەك چوڭ - + Tree دەرەخسىمان كۆرۈنۈش - + Default view: سۈكۈتتىكى كۆرۈنۈش: - + Icon سىن بەلگە كۆرۈنۈشى - + List تىزىملىك كۆرۈنۈشى - + Restore default view mode for all directories بارلىق مۇندەرىجىنى سۈكۈتتىكى كۆرۈش ھالىتىگە قايتۇرۇش - + Restore default view mode سۈكۈتتىكى كۆرۈش ھالىتىگە قايتۇرۇش - + Thumbnail preview قىسقارتما سۈرەت كۆرۈنۈشى - + Compressed file preview بولاق ھۆججەتنى كۆرۈپ بېقىش - + Text preview تېكىستنى كۆرۈپ بېقىش - + Document preview ھۆججەتنى كۆرۈپ بېقىش - + Image preview سۈرەتنى كۆرۈپ بېقىش - + Video preview سىننى كۆرۈپ بېقىش - + Music preview مۇزىكا ئۇچۇرلىىرنى كۆرۈپ بېقىش - + The remote environment shows thumbnail previews يىراق مۇساپىلىك مۇھىتتا قىسقارتىلما سۈرەت كۆرۈنسۇن - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze قىسقارتىلما سۈرەتنى كۆرۈش ئىقتىدارىنى ئاچسىڭىز يىراق مۇساپىلىك مۇندەرىجىنىڭ يۈكلىنىشى ئاستىلاپ كېتىشى ياكى قېتىۋېلىشى مۇمكىن - + Advanced ئالىي تەڭشەك - + Mount ئاغدۇرۇش - + Auto mount ئاپتوماتىك ئاغدۇرۇش - + Open after auto mount ئاپتوماتىك ئاغدۇرغاندىن كېيىن ئېچىش - + Merge the entries of Samba shared folders Samba ھەمبەھىر مۇندەرىجىسىنىڭ كىرىش ئېغىزىنى بىرىكتۈرۈپ كۆرسىتىش - + Switching the entry display may lead to failed mounting كىرىش ئېغىزىنى كۆرسىتىش ھالىتىنى ئالماشتۇرسىڭىز قاچىلاش ئۈنۈمسىز بولۇپ قېلىشى مۇمكىن - + Use the file chooser dialog of File Manager ھۆججەت باشقۇرغۇچىنىڭ ھۆججەت تاللاش سۆزلىشىش رامكىسىنى ئىشلىتىڭ - + Ask for my confirmation when deleting files ئادەتتىكى ئۆچۈرۈش ئەسكەرتمىسىنى ئېچىش @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address ئادرېسنى ئىزدەڭ ياكى كىرگۈزۈڭ @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search ئىزدەش - + advanced search ئالىي ئىزدەش diff --git a/translations/dde-file-manager_zh_CN.ts b/translations/dde-file-manager_zh_CN.ts index 24c9a60a0a..293a32bf18 100644 --- a/translations/dde-file-manager_zh_CN.ts +++ b/translations/dde-file-manager_zh_CN.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed 设备已被安全移除 - + eject 弹出 - + unmount 卸载 - - + + remove 移除 - + Operation failed 操作失败 - + Device (%1) is busy, cannot %2 now. 设备(%1)正忙,无法%2。 @@ -1716,6 +1716,19 @@ or + + + + + + + + + + + Clear search history + 清空搜索记录 + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button 确 定 @@ -2549,8 +2563,9 @@ - - + + + Cancel button 取 消 @@ -2579,7 +2594,7 @@ - + Run button 运 行 @@ -2723,22 +2738,27 @@ 访问%1失败 - + + Are you sure clear search histories? + 确认清空搜索记录? + + + %1 that this shortcut refers to has been changed or moved 此快捷方式所指向的“%1”已被更改或移动 - + Do you want to delete this shortcut? 是否删除此快捷方式? - + This file is not executable, do you want to add the execute permission and run? 此文件没有可执行权限,是否添加权限并运行? - + The selected files contain system file/directory, and it cannot be deleted 选择的文件中包含系统目录,不能被删除 @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window 总是在新窗口打开文件夹 - + Open file: 打开文件: - + Click 单击 - + Double click 双击 - + New window and tab 新窗口和新标签 - + Open from default window: 从默认窗口打开: - - + + Computer 计算机 - - + + Home 主目录 - - + + Desktop 桌面 - - + + Videos 视频 - - + + Music 音乐 - - + + Pictures 图片 - - + + Documents 文档 - - + + Downloads 下载 - + Open in new tab: 从新标签打开: - + Current Directory 当前目录 - + Files and folders 文件和目录 - + Show hidden files 显示隐藏文件 - + Show file extensions 显示文件扩展名 - + Mix sorting of files and folders 文件和文件夹混合排序 - + + Others + 其他 + + + + Display search history + 显示搜索记录 + + + Workspace 工作区 - + View 视图 - + Default size: 默认大小: - + Extra small 特小 - + Small - + Medium - + Large - + Extra large 特大 - + Tree 树形视图 - + Default view: 默认视图: - + Icon 图标视图 - + List 列表视图 - + Restore default view mode for all directories 所有目录恢复默认视图 - + Restore default view mode 恢复默认视图 - + Thumbnail preview 缩略图预览 - + Compressed file preview 压缩文件预览 - + Text preview 文本预览 - + Document preview 文档预览 - + Image preview 图片预览 - + Video preview 视频预览 - + Music preview 音乐预览 - + The remote environment shows thumbnail previews 远程环境显示缩略图预览 - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze 开启缩略图预览可能导致远程目录加载缓慢或操作卡顿 - + Advanced 高级设置 - + Mount 挂载 - + Auto mount 自动挂载 - + Open after auto mount 自动挂载后打开 - + Merge the entries of Samba shared folders 合并显示Samba共享目录入口 - + Switching the entry display may lead to failed mounting 切换入口显示模式可能会导致挂载状态失效 - + Use the file chooser dialog of File Manager 使用文件管理器的文件选择对话框 - + Ask for my confirmation when deleting files 开启普通删除提示 @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address 搜索或输入地址 @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search 搜索 - + advanced search 高级搜索 diff --git a/translations/dde-file-manager_zh_HK.ts b/translations/dde-file-manager_zh_HK.ts index 2a83fb0eb8..d0bd7e6dd2 100644 --- a/translations/dde-file-manager_zh_HK.ts +++ b/translations/dde-file-manager_zh_HK.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed 設備已被安全移除 - + eject 彈出 - + unmount 卸載 - - + + remove 移除 - + Operation failed 操作失敗 - + Device (%1) is busy, cannot %2 now. 設備(%1)正忙,無法%2。 @@ -1716,6 +1716,19 @@ or + + + + + + + + + + + Clear search history + + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button 確 定 @@ -2549,8 +2563,9 @@ - - + + + Cancel button 取 消 @@ -2579,7 +2594,7 @@ - + Run button 運 行 @@ -2723,22 +2738,27 @@ 訪問%1失敗 - + + Are you sure clear search histories? + + + + %1 that this shortcut refers to has been changed or moved 此快捷方式所指向的“%1”已被更改或移動 - + Do you want to delete this shortcut? 是否刪除此快捷方式? - + This file is not executable, do you want to add the execute permission and run? 此文件沒有可執行權限,是否添加權限並運行? - + The selected files contain system file/directory, and it cannot be deleted 選擇的文件中包含系統目錄,不能被刪除 @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window 總是在新窗口打開文件夾 - + Open file: 打開文件: - + Click 單擊 - + Double click 雙擊 - + New window and tab 新窗口和新標籤 - + Open from default window: 從默認窗口打開: - - + + Computer 計算機 - - + + Home 主目錄 - - + + Desktop 桌 面 - - + + Videos 影片 - - + + Music 音樂 - - + + Pictures 圖片 - - + + Documents 文檔 - - + + Downloads 下載 - + Open in new tab: 從新標籤打開: - + Current Directory 當前目錄 - + Files and folders 文件和目錄 - + Show hidden files 顯示隱藏文件 - + Show file extensions 顯示文件擴展名 - + Mix sorting of files and folders 文件和文件夾混合排序 - + + Others + 其他 + + + + Display search history + + + + Workspace 工作區 - + View 視圖 - + Default size: 默認大小: - + Extra small 極小 - + Small - + Medium - + Large - + Extra large 極大 - + Tree 樹形視圖 - + Default view: 默認視圖: - + Icon 圖標視圖 - + List 列表視圖 - + Restore default view mode for all directories 所有目錄恢復默認視圖 - + Restore default view mode 恢復默認視圖 - + Thumbnail preview 縮略圖預覽 - + Compressed file preview 壓縮文件預覽 - + Text preview 文本預覽 - + Document preview 文檔預覽 - + Image preview 圖片預覽 - + Video preview 影片預覽 - + Music preview 音樂預覽 - + The remote environment shows thumbnail previews 遠程環境顯示縮略圖預覽 - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze 開啟縮略圖預覽可能導致遠程目錄加載緩慢或操作卡頓 - + Advanced 高級設置 - + Mount 掛載 - + Auto mount 自動掛載 - + Open after auto mount 自動掛載後打開 - + Merge the entries of Samba shared folders 合併顯示Samba共享目錄入口 - + Switching the entry display may lead to failed mounting 切換入口顯示模式可能會導致掛載狀態失效 - + Use the file chooser dialog of File Manager 使用檔案管理員的文件選擇對話框 - + Ask for my confirmation when deleting files 開啟普通刪除提示 @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address 搜索或輸入地址 @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search 搜索 - + advanced search 高級搜索 diff --git a/translations/dde-file-manager_zh_TW.ts b/translations/dde-file-manager_zh_TW.ts index f535324738..8f47ae1c7b 100644 --- a/translations/dde-file-manager_zh_TW.ts +++ b/translations/dde-file-manager_zh_TW.ts @@ -64,33 +64,33 @@ DockItemDataManager - + The device has been safely removed 裝置已被安全移除 - + eject 彈出 - + unmount 移除 - - + + remove 移除 - + Operation failed 操作失敗 - + Device (%1) is busy, cannot %2 now. 裝置(%1)正忙,無法%2。 @@ -1716,6 +1716,19 @@ or + + + + + + + + + + + Clear search history + + ddplugin_canvas::CanvasMenuScene @@ -2410,7 +2423,8 @@ - + + Confirm button @@ -2520,7 +2534,7 @@ - + OK button 確 定 @@ -2549,8 +2563,9 @@ - - + + + Cancel button 取 消 @@ -2579,7 +2594,7 @@ - + Run button 運 行 @@ -2723,22 +2738,27 @@ 訪問%1失敗 - + + Are you sure clear search histories? + + + + %1 that this shortcut refers to has been changed or moved 此捷徑所指向的“%1”已被更改或移動 - + Do you want to delete this shortcut? 是否刪除此捷徑? - + This file is not executable, do you want to add the execute permission and run? 此文件沒有可執行權限,是否添加權限並執行? - + The selected files contain system file/directory, and it cannot be deleted 選擇的文件中包含系統目錄,不能被刪除 @@ -2877,265 +2897,275 @@ dfmbase::SettingBackend - + Always open folder in new window 永遠在新視窗打開 - + Open file: 文件打開方式: - + Click 單擊 - + Double click 雙擊 - + New window and tab 新視窗與新分頁 - + Open from default window: 視窗預設打開: - - + + Computer 電腦 - - + + Home 主資料夾 - - + + Desktop 顯示桌面 - - + + Videos 我的影片 - - + + Music 音樂 - - + + Pictures 圖片 - - + + Documents 文件 - - + + Downloads 下載 - + Open in new tab: 從新分頁打開: - + Current Directory 目前目錄 - + Files and folders 文件和目錄 - + Show hidden files 顯示隱藏檔案 - + Show file extensions 顯示文件副檔名 - + Mix sorting of files and folders 文件和資料夾混合排序 - + + Others + 其他 + + + + Display search history + + + + Workspace 工作區 - + View 檢視 - + Default size: 預設大小: - + Extra small 最小 - + Small - + Medium - + Large - + Extra large 最大 - + Tree 樹形檢視 - + Default view: 預設顯示 - + Icon 圖示顯示 - + List 列表顯示 - + Restore default view mode for all directories 所有目錄復原預設檢視 - + Restore default view mode 復原預設檢視 - + Thumbnail preview 縮圖預覽 - + Compressed file preview 壓縮檔案預覽 - + Text preview 預覽文字 - + Document preview 預覽文件 - + Image preview 預覽圖片 - + Video preview 預覽影片 - + Music preview 音樂預覽 - + The remote environment shows thumbnail previews 遠端環境顯示縮圖預覽 - + Turning on the thumbnail preview may cause the remote directory to load slowly or the operation to freeze 開啟縮圖預覽可能導致遠端目錄載入緩慢或操作卡頓 - + Advanced 進階 - + Mount 掛載 - + Auto mount 自動掛載 - + Open after auto mount 自動掛載後開啟 - + Merge the entries of Samba shared folders 合併顯示Samba共享目錄入口 - + Switching the entry display may lead to failed mounting 切換入口顯示模式可能會導致掛載狀態失效 - + Use the file chooser dialog of File Manager 使用文件管理器的選擇文件對話框 - + Ask for my confirmation when deleting files 打開普通刪除提示 @@ -5306,7 +5336,7 @@ dfmplugin_titlebar::AddressBarPrivate - + Search or enter address 搜尋或輸入地址 @@ -5532,12 +5562,12 @@ dfmplugin_titlebar::TitleBarWidget - + search 搜尋 - + advanced search 進階搜尋