diff --git a/NEWS.md b/NEWS.md index ad136f5bb9..cb95368ad2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ### Unreleased +* Added action search popup on Ctrl+Shift+P (with dogboydog, #3449) * Fixed new layer names to be always unique (by Logan Higinbotham, #3452) * Scripting: Added Object.setColorProperty and Object.setFloatProperty (#3423) * Scripting: Added tiled.projectFilePath diff --git a/docs/conf.py b/docs/conf.py index d0c190943e..e933cfa33c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # Options for localization locale_dirs = ['locale/'] diff --git a/docs/manual/introduction.rst b/docs/manual/introduction.rst index 9baa62d090..eda1e852b6 100644 --- a/docs/manual/introduction.rst +++ b/docs/manual/introduction.rst @@ -63,15 +63,22 @@ as well as to be able to quickly switch between multiple projects, it is recommended to first set up a :doc:`Tiled project `. This is however an entirely optional step that can be skipped when desired. -Choose *Project -> Save Project As...* to save a new project file. The -recommended location is the root of your project, but you can place it -anywhere you want. - -Next, we'll add at least one folder, either some "assets" folder or simply the -root of your project, but you can also choose to add several top-level folders -like "tilesets", "maps", "templates", etc. Right-click in the Project view and -choose *Add Folder to Project...* to add the relevant folders. - +Choose *File -> New -> New Project...* to create a new project file. It is +recommended to save this file in the root of your project. The directory in +which you store the project will be automatically added, so that its files are +visible in the Project view. + +When necessary, you can add additional folders to the project or replace the +one added by default. For example, you could choose to add several top-level +folders like "tilesets", "maps", "templates", etc. Right-click in the +Project view and choose *Add Folder to Project...* to add the +relevant folders. + +.. hint:: + + You can press ``Ctrl+Shift+P`` to open the action search widget, + which can provide a faster way to get to actions than looking for them in + the menus! Creating a New Map ~~~~~~~~~~~~~~~~~~ diff --git a/docs/manual/keyboard-shortcuts.rst b/docs/manual/keyboard-shortcuts.rst index 7ecedb51ab..5ed39fbcc6 100644 --- a/docs/manual/keyboard-shortcuts.rst +++ b/docs/manual/keyboard-shortcuts.rst @@ -14,6 +14,7 @@ General - ``Ctrl + N`` - Create a new map - ``Ctrl + O`` - Open any file or project - ``Ctrl + P`` - Open a file in the current project +- ``Ctrl + Shift + P`` - Search for available actions - ``Ctrl + Shift + T`` - Reopen a recently closed file - ``Ctrl + S`` - Save current document - ``Ctrl + Alt + S`` - Save current document to another file diff --git a/src/tiled/abstractobjecttool.cpp b/src/tiled/abstractobjecttool.cpp index a552f26023..0510b781cb 100644 --- a/src/tiled/abstractobjecttool.cpp +++ b/src/tiled/abstractobjecttool.cpp @@ -130,9 +130,23 @@ AbstractObjectTool::AbstractObjectTool(Id id, connect(mRotateLeft, &QAction::triggered, this, &AbstractObjectTool::rotateLeft); connect(mRotateRight, &QAction::triggered, this, &AbstractObjectTool::rotateRight); + setActionsEnabled(false); + AbstractObjectTool::languageChanged(); } +void AbstractObjectTool::activate(MapScene *scene) +{ + AbstractTool::activate(scene); + setActionsEnabled(true); +} + +void AbstractObjectTool::deactivate(MapScene *scene) +{ + setActionsEnabled(false); + AbstractTool::deactivate(scene); +} + void AbstractObjectTool::keyPressed(QKeyEvent *event) { switch (event->key()) { @@ -739,4 +753,12 @@ void AbstractObjectTool::showContextMenu(MapObject *clickedObject, } } +void AbstractObjectTool::setActionsEnabled(bool enabled) +{ + mFlipHorizontal->setEnabled(enabled); + mFlipVertical->setEnabled(enabled); + mRotateLeft->setEnabled(enabled); + mRotateRight->setEnabled(enabled); +} + #include "moc_abstractobjecttool.cpp" diff --git a/src/tiled/abstractobjecttool.h b/src/tiled/abstractobjecttool.h index 0c2817bfef..8cca5ce5c1 100644 --- a/src/tiled/abstractobjecttool.h +++ b/src/tiled/abstractobjecttool.h @@ -59,6 +59,9 @@ class AbstractObjectTool : public AbstractTool const QKeySequence &shortcut, QObject *parent = nullptr); + void activate(MapScene *scene) override; + void deactivate(MapScene *scene) override; + void keyPressed(QKeyEvent *event) override; void mouseLeft() override; void mouseMoved(const QPointF &pos, Qt::KeyboardModifiers modifiers) override; @@ -107,6 +110,8 @@ class AbstractObjectTool : public AbstractTool void showContextMenu(MapObject *clickedObject, QPoint screenPos); + void setActionsEnabled(bool enabled); + QAction *mFlipHorizontal; QAction *mFlipVertical; QAction *mRotateLeft; diff --git a/src/tiled/abstracttilefilltool.cpp b/src/tiled/abstracttilefilltool.cpp index 6e2430643a..aac1c90200 100644 --- a/src/tiled/abstracttilefilltool.cpp +++ b/src/tiled/abstracttilefilltool.cpp @@ -60,9 +60,16 @@ AbstractTileFillTool::~AbstractTileFillTool() { } +void AbstractTileFillTool::activate(MapScene *scene) +{ + AbstractTileTool::activate(scene); + mStampActions->setEnabled(true); +} + void AbstractTileFillTool::deactivate(MapScene *scene) { mCaptureStampHelper.reset(); + mStampActions->setEnabled(false); AbstractTileTool::deactivate(scene); } diff --git a/src/tiled/abstracttilefilltool.h b/src/tiled/abstracttilefilltool.h index b7d4e4fea3..97a7e6ef41 100644 --- a/src/tiled/abstracttilefilltool.h +++ b/src/tiled/abstracttilefilltool.h @@ -53,6 +53,7 @@ class AbstractTileFillTool : public AbstractTileTool QObject *parent = nullptr); ~AbstractTileFillTool() override; + void activate(MapScene *scene) override; void deactivate(MapScene *scene) override; void mousePressed(QGraphicsSceneMouseEvent *event) override; diff --git a/src/tiled/actionmanager.cpp b/src/tiled/actionmanager.cpp index 81c9ea3338..63c9565042 100644 --- a/src/tiled/actionmanager.cpp +++ b/src/tiled/actionmanager.cpp @@ -161,6 +161,19 @@ QAction *ActionManager::findAction(Id id) return d->mIdToActions.value(id); } +QAction *ActionManager::findEnabledAction(Id id) +{ + auto d = instance(); + + const auto [start, end] = qAsConst(d->mIdToActions).equal_range(id); + for (auto it = start; it != end; ++it) { + if (it.value()->isEnabled()) + return it.value(); + } + + return nullptr; +} + bool ActionManager::hasMenu(Id id) { return instance()->mIdToMenu.contains(id); diff --git a/src/tiled/actionmanager.h b/src/tiled/actionmanager.h index 8eafc6d85e..bfc52bc0a2 100644 --- a/src/tiled/actionmanager.h +++ b/src/tiled/actionmanager.h @@ -80,6 +80,7 @@ class ActionManager : public QObject static QAction *action(Id id); static QAction *findAction(Id id); + static QAction *findEnabledAction(Id id); static bool hasMenu(Id id); diff --git a/src/tiled/actionsearch.cpp b/src/tiled/actionsearch.cpp new file mode 100644 index 0000000000..d1267c0712 --- /dev/null +++ b/src/tiled/actionsearch.cpp @@ -0,0 +1,312 @@ +/* + * actionsearch.cpp + * Copyright 2022, Chris Boehm AKA dogboydog + * Copyright 2022, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "actionsearch.h" + +#include "actionmanager.h" +#include "utils.h" + +#include +#include +#include +#include +#include + +namespace Tiled { + +static QFont scaledFont(const QFont &font, qreal scale) +{ + QFont scaled(font); + if (font.pixelSize() > 0) + scaled.setPixelSize(font.pixelSize() * scale); + else + scaled.setPointSizeF(font.pointSizeF() * scale); + return scaled; +} + +class ActionMatchDelegate : public QStyledItemDelegate +{ +public: + ActionMatchDelegate(QObject *parent = nullptr); + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setWords(const QStringList &words) { mWords = words; } + +private: + class Fonts { + public: + Fonts(const QFont &base) + : small(scaledFont(base, 0.9)) + , big(scaledFont(base, 1.1)) + {} + + const QFont small; + const QFont big; + }; + + QStringList mWords; +}; + +ActionMatchDelegate::ActionMatchDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{} + +QSize ActionMatchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const +{ + const QFont bigFont = scaledFont(option.font, 1.2); + const QFontMetrics bigFontMetrics(bigFont); + + const int margin = Utils::dpiScaled(2); + return QSize(margin * 2, margin * 2 + bigFontMetrics.lineSpacing()); +} + +void ActionMatchDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + painter->save(); + + const QString name = index.data().toString(); + +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + const auto ranges = Utils::matchingRanges(mWords, &name); +#else + const auto ranges = Utils::matchingRanges(mWords, name); +#endif + + QString nameHtml; + int nameIndex = 0; + + auto nameRange = [&] (int first, int last) -> QStringRef { +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) + return QStringView(name).mid(first, last - first + 1); +#else + return name.midRef(first, last - first + 1); +#endif + }; + + for (const auto &range : ranges) { + if (range.first > nameIndex) + nameHtml.append(nameRange(nameIndex, range.first - 1)); + + nameHtml.append(QStringLiteral("")); + nameHtml.append(nameRange(range.first, range.second)); + nameHtml.append(QStringLiteral("")); + + nameIndex = range.second + 1; + } + + nameHtml.append(nameRange(nameIndex, name.size() - 1)); + + const Fonts fonts(option.font); + + const int margin = Utils::dpiScaled(2); + const int iconSize = option.rect.height() - margin * 2; + const auto nameRect = option.rect.adjusted(margin * 4 + iconSize, margin, -margin, 0); + const auto shortcutRect = option.rect.adjusted(0, margin, -margin, -margin); + + // draw the background (covers selection) + QStyle *style = QApplication::style(); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter); + + // adjust text color to state + QPalette::ColorGroup cg = option.state & QStyle::State_Enabled + ? QPalette::Normal : QPalette::Disabled; + if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) + cg = QPalette::Inactive; + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(cg, QPalette::HighlightedText)); + } else { + painter->setPen(option.palette.color(cg, QPalette::Text)); + } + + QTextOption textOption; + textOption.setWrapMode(QTextOption::NoWrap); + + QStaticText staticText(nameHtml); + staticText.setTextOption(textOption); + staticText.setTextFormat(Qt::RichText); + staticText.prepare(painter->transform(), fonts.big); + + painter->setFont(fonts.big); + painter->drawStaticText(nameRect.topLeft(), staticText); + + const QIcon icon = index.data(Qt::DecorationRole).value(); + if (!icon.isNull()) { + const auto iconRect = QRect(option.rect.topLeft() + QPoint(margin, margin), + QSize(iconSize, iconSize)); + + icon.paint(painter, iconRect); + } + + const QKeySequence shortcut = index.data(ActionLocatorSource::ShortcutRole).value(); + if (!shortcut.isEmpty()) { + const QString shortcutText = shortcut.toString(QKeySequence::NativeText); + const QFontMetrics smallFontMetrics(fonts.small); + + staticText.setTextFormat(Qt::PlainText); + staticText.setText(shortcutText); + staticText.prepare(painter->transform(), fonts.small); + + const int centeringMargin = (shortcutRect.height() - smallFontMetrics.height()) / 2; + painter->setOpacity(0.75); + painter->setFont(fonts.small); + painter->drawStaticText(shortcutRect.right() - staticText.size().width() - centeringMargin, + shortcutRect.top() + centeringMargin, + staticText); + } + + // draw the focus rect + if (option.state & QStyle::State_HasFocus) { + QStyleOptionFocusRect o; + o.QStyleOption::operator=(option); + o.rect = style->subElementRect(QStyle::SE_ItemViewItemFocusRect, &option); + o.state |= QStyle::State_KeyboardFocusChange; + o.state |= QStyle::State_Item; + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) + ? QPalette::Normal : QPalette::Disabled; + o.backgroundColor = option.palette.color(cg, (option.state & QStyle::State_Selected) + ? QPalette::Highlight : QPalette::Window); + style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter); + } + + painter->restore(); +} + +/////////////////////////////////////////////////////////////////////////////// + +ActionLocatorSource::ActionLocatorSource(QObject *parent) + : LocatorSource(parent) + , mDelegate(new ActionMatchDelegate(this)) +{} + +int ActionLocatorSource::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : mMatches.size(); +} + +QVariant ActionLocatorSource::data(const QModelIndex &index, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + const Match &match = mMatches.at(index.row()); + return match.text; + } + case Qt::DecorationRole: { + const Match &match = mMatches.at(index.row()); + if (auto action = ActionManager::findAction(match.actionId)) + return action->icon(); + break; + } + case ShortcutRole: { + const Match &match = mMatches.at(index.row()); + if (auto action = ActionManager::findAction(match.actionId)) + return action->shortcut(); + break; + } + } + return QVariant(); +} + +QAbstractItemDelegate *ActionLocatorSource::delegate() const +{ + return mDelegate; +} + +QString ActionLocatorSource::placeholderText() const +{ + return QCoreApplication::translate("Tiled::LocatorWidget", "Search actions..."); +} + +QVector ActionLocatorSource::findActions(const QStringList &words) +{ + const QRegularExpression re(QLatin1String("(?<=^|[^&])&")); + const QList actions = ActionManager::actions(); + const Id searchActionsId("SearchActions"); + + QVector result; + + for (const Id &actionId : actions) { + if (actionId == searchActionsId) + continue; + + const auto action = ActionManager::findEnabledAction(actionId); + if (!action) + continue; + + // remove single & characters + QString sanitizedText = action->text(); + sanitizedText.replace(re, QString()); + +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) + const int totalScore = Utils::matchingScore(words, sanitizedText); +#else + const int totalScore = Utils::matchingScore(words, &sanitizedText); +#endif + + if (totalScore > 0) { + result.append(Match { + totalScore, + actionId, + sanitizedText + }); + } + } + + return result; +} + +void ActionLocatorSource::setFilterWords(const QStringList &words) +{ + auto matches = findActions(words); + + std::stable_sort(matches.begin(), matches.end(), [] (const Match &a, const Match &b) { + // Sort based on score first + if (a.score != b.score) + return a.score > b.score; + + // If score is the same, sort alphabetically + return a.text.compare(b.text, Qt::CaseInsensitive) < 0; + }); + + mDelegate->setWords(words); + + beginResetModel(); + mMatches = std::move(matches); + endResetModel(); +} + +void ActionLocatorSource::activate(const QModelIndex &index) +{ + const Id actionId = mMatches.at(index.row()).actionId; + if (auto action = ActionManager::findEnabledAction(actionId)) + action->trigger(); +} + +} // namespace Tiled + +#include "moc_actionsearch.cpp" diff --git a/src/tiled/actionsearch.h b/src/tiled/actionsearch.h new file mode 100644 index 0000000000..4503cf8173 --- /dev/null +++ b/src/tiled/actionsearch.h @@ -0,0 +1,64 @@ +/* + * actionsearch.h + * Copyright 2022, Chris Boehm AKA dogboydog + * Copyright 2022, Thorbjørn Lindeijer + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "id.h" +#include "locatorwidget.h" + +namespace Tiled { + +class ActionMatchDelegate; + +class ActionLocatorSource : public LocatorSource +{ + Q_OBJECT + +public: + enum { + ShortcutRole = Qt::UserRole + }; + + explicit ActionLocatorSource(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // LocatorSource + QAbstractItemDelegate *delegate() const override; + QString placeholderText() const override; + void setFilterWords(const QStringList &words) override; + void activate(const QModelIndex &index) override; + +private: + struct Match { + int score; + Id actionId; + QString text; + }; + + ActionMatchDelegate *mDelegate; + QVector mMatches; + + static QVector findActions(const QStringList &words); +}; + +} // namespace Tiled diff --git a/src/tiled/images/16/edit-find.png b/src/tiled/images/16/edit-find.png new file mode 100644 index 0000000000..05c6821704 Binary files /dev/null and b/src/tiled/images/16/edit-find.png differ diff --git a/src/tiled/images/22/edit-find.png b/src/tiled/images/22/edit-find.png new file mode 100644 index 0000000000..8cc0349cab Binary files /dev/null and b/src/tiled/images/22/edit-find.png differ diff --git a/src/tiled/images/24/edit-find.png b/src/tiled/images/24/edit-find.png new file mode 100644 index 0000000000..afb9588614 Binary files /dev/null and b/src/tiled/images/24/edit-find.png differ diff --git a/src/tiled/images/32/edit-find.png b/src/tiled/images/32/edit-find.png new file mode 100644 index 0000000000..8bd61a0e63 Binary files /dev/null and b/src/tiled/images/32/edit-find.png differ diff --git a/src/tiled/images/48/edit-find.png b/src/tiled/images/48/edit-find.png new file mode 100644 index 0000000000..6d1db191e7 Binary files /dev/null and b/src/tiled/images/48/edit-find.png differ diff --git a/src/tiled/libtilededitor.qbs b/src/tiled/libtilededitor.qbs index 422df509d7..03fd3babe1 100644 --- a/src/tiled/libtilededitor.qbs +++ b/src/tiled/libtilededitor.qbs @@ -91,6 +91,8 @@ DynamicLibrary { "abstractworldtool.h", "actionmanager.cpp", "actionmanager.h", + "actionsearch.cpp", + "actionsearch.h", "addpropertydialog.cpp", "addpropertydialog.h", "addpropertydialog.ui", diff --git a/src/tiled/locatorwidget.cpp b/src/tiled/locatorwidget.cpp index e4d23186f2..82a161761d 100644 --- a/src/tiled/locatorwidget.cpp +++ b/src/tiled/locatorwidget.cpp @@ -23,11 +23,8 @@ #include "documentmanager.h" #include "filteredit.h" #include "projectmanager.h" -#include "projectmodel.h" -#include "rangeset.h" #include "utils.h" -#include #include #include #include @@ -42,50 +39,6 @@ namespace Tiled { -class MatchesModel : public QAbstractListModel -{ -public: - explicit MatchesModel(QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - const QVector &matches() const { return mMatches; } - void setMatches(QVector matches); - -private: - QVector mMatches; -}; - -MatchesModel::MatchesModel(QObject *parent) - : QAbstractListModel(parent) -{} - -int MatchesModel::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : mMatches.size(); -} - -QVariant MatchesModel::data(const QModelIndex &index, int role) const -{ - switch (role) { - case Qt::DisplayRole: { - const ProjectModel::Match &match = mMatches.at(index.row()); - return match.relativePath().toString(); - } - } - return QVariant(); -} - -void MatchesModel::setMatches(QVector matches) -{ - beginResetModel(); - mMatches = std::move(matches); - endResetModel(); -} - -/////////////////////////////////////////////////////////////////////////////// - static QFont scaledFont(const QFont &font, qreal scale) { QFont scaled(font); @@ -96,13 +49,13 @@ static QFont scaledFont(const QFont &font, qreal scale) return scaled; } -class MatchDelegate : public QStyledItemDelegate +class FileMatchDelegate : public QStyledItemDelegate { public: - MatchDelegate(QObject *parent = nullptr); + FileMatchDelegate(QObject *parent = nullptr); QSize sizeHint(const QStyleOptionViewItem &option, - const QModelIndex &index) const override; + const QModelIndex &index) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, @@ -125,11 +78,12 @@ class MatchDelegate : public QStyledItemDelegate QStringList mWords; }; -MatchDelegate::MatchDelegate(QObject *parent) +FileMatchDelegate::FileMatchDelegate(QObject *parent) : QStyledItemDelegate(parent) {} -QSize MatchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const +QSize FileMatchDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &) const { const QFont bigFont = scaledFont(option.font, 1.2); const QFontMetrics bigFontMetrics(bigFont); @@ -138,9 +92,9 @@ QSize MatchDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIn return QSize(margin * 2, margin * 2 + bigFontMetrics.lineSpacing() * 2); } -void MatchDelegate::paint(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const +void FileMatchDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const { painter->save(); @@ -219,7 +173,6 @@ void MatchDelegate::paint(QPainter *painter, QStaticText staticText(fileNameHtml); staticText.setTextOption(textOption); staticText.setTextFormat(Qt::RichText); - staticText.setTextWidth(fileNameRect.width()); staticText.prepare(painter->transform(), fonts.big); painter->setFont(fonts.big); @@ -290,7 +243,7 @@ void ResultsView::updateMaximumHeight() setMaximumHeight(maximumHeight); } -inline void ResultsView::keyPressEvent(QKeyEvent *event) +void ResultsView::keyPressEvent(QKeyEvent *event) { // Make sure the Enter and Return keys activate the current index. This // doesn't happen otherwise on macOS. @@ -307,24 +260,26 @@ inline void ResultsView::keyPressEvent(QKeyEvent *event) /////////////////////////////////////////////////////////////////////////////// -LocatorWidget::LocatorWidget(QWidget *parent) +LocatorWidget::LocatorWidget(LocatorSource *locatorSource, + QWidget *parent) : QFrame(parent, Qt::Popup) + , mLocatorSource(locatorSource) , mFilterEdit(new FilterEdit(this)) , mResultsView(new ResultsView(this)) - , mListModel(new MatchesModel(this)) - , mDelegate(new MatchDelegate(this)) { setAttribute(Qt::WA_DeleteOnClose); setFrameStyle(QFrame::StyledPanel | QFrame::Plain); + mLocatorSource->setParent(this); // take ownership of source + mResultsView->setUniformRowHeights(true); mResultsView->setRootIsDecorated(false); - mResultsView->setItemDelegate(mDelegate); + mResultsView->setItemDelegate(mLocatorSource->delegate()); mResultsView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - mResultsView->setModel(mListModel); + mResultsView->setModel(mLocatorSource); mResultsView->setHeaderHidden(true); - mFilterEdit->setPlaceholderText(tr("Filename")); + mFilterEdit->setPlaceholderText(mLocatorSource->placeholderText()); mFilterEdit->setFilteredView(mResultsView); mFilterEdit->setClearTextOnEscape(false); mFilterEdit->setFont(scaledFont(mFilterEdit->font(), 1.5)); @@ -346,9 +301,8 @@ LocatorWidget::LocatorWidget(QWidget *parent) connect(mFilterEdit, &QLineEdit::textChanged, this, &LocatorWidget::setFilterText); connect(mResultsView, &QAbstractItemView::activated, this, [this] (const QModelIndex &index) { - const QString file = mListModel->matches().at(index.row()).path; close(); - DocumentManager::instance()->openFile(file); + mLocatorSource->activate(index); }); } @@ -375,6 +329,60 @@ void LocatorWidget::setFilterText(const QString &text) const QStringList words = normalized.split(QLatin1Char(' '), Qt::SkipEmptyParts); #endif + mLocatorSource->setFilterWords(words); + + mResultsView->updateGeometry(); + mResultsView->updateMaximumHeight(); + + // Restore or introduce selection + if (auto index = mLocatorSource->index(0, 0); index.isValid()) + mResultsView->setCurrentIndex(index); + + layout()->activate(); + resize(sizeHint()); +} + +/////////////////////////////////////////////////////////////////////////////// + +FileLocatorSource::FileLocatorSource(QObject *parent) + : LocatorSource(parent) + , mDelegate(new FileMatchDelegate(this)) +{} + +int FileLocatorSource::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : mMatches.size(); +} + +QVariant FileLocatorSource::data(const QModelIndex &index, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + const ProjectModel::Match &match = mMatches.at(index.row()); + return match.relativePath().toString(); + } + } + return QVariant(); +} + +QAbstractItemDelegate *FileLocatorSource::delegate() const +{ + return mDelegate; +} + +QString FileLocatorSource::placeholderText() const +{ + return QCoreApplication::translate("Tiled::LocatorWidget", "Filename"); +} + +void FileLocatorSource::activate(const QModelIndex &index) +{ + const QString file = mMatches.at(index.row()).path; + DocumentManager::instance()->openFile(file); +} + +void FileLocatorSource::setFilterWords(const QStringList &words) +{ auto projectModel = ProjectManager::instance()->projectModel(); auto matches = projectModel->findFiles(words); @@ -388,17 +396,10 @@ void LocatorWidget::setFilterText(const QString &text) }); mDelegate->setWords(words); - mListModel->setMatches(matches); - mResultsView->updateGeometry(); - mResultsView->updateMaximumHeight(); - - // Restore or introduce selection - if (!matches.isEmpty()) - mResultsView->setCurrentIndex(mListModel->index(0)); - - layout()->activate(); - resize(sizeHint()); + beginResetModel(); + mMatches = std::move(matches); + endResetModel(); } } // namespace Tiled diff --git a/src/tiled/locatorwidget.h b/src/tiled/locatorwidget.h index c09cb6b7e6..9436b5e969 100644 --- a/src/tiled/locatorwidget.h +++ b/src/tiled/locatorwidget.h @@ -20,31 +20,69 @@ #pragma once +#include #include +#include "projectmodel.h" + +class QAbstractItemDelegate; + namespace Tiled { class FilterEdit; -class MatchDelegate; -class MatchesModel; +class FileMatchDelegate; class ResultsView; +/** + * Interface for providing a source of locator items based on filter words. +*/ +class LocatorSource : public QAbstractListModel +{ +public: + explicit LocatorSource(QObject *parent = nullptr) + : QAbstractListModel(parent) + {} + + virtual QAbstractItemDelegate *delegate() const = 0; + virtual QString placeholderText() const = 0; + virtual void setFilterWords(const QStringList &words) = 0; + virtual void activate(const QModelIndex &index) = 0; +}; + class LocatorWidget : public QFrame { Q_OBJECT public: - explicit LocatorWidget(QWidget *parent = nullptr); - + explicit LocatorWidget(LocatorSource *locatorSource, + QWidget *parent = nullptr); void setVisible(bool visible) override; private: void setFilterText(const QString &text); + LocatorSource *mLocatorSource; FilterEdit *mFilterEdit; ResultsView *mResultsView; - MatchesModel *mListModel; - MatchDelegate *mDelegate; +}; + +class FileLocatorSource : public LocatorSource +{ +public: + explicit FileLocatorSource(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // LocatorSource + QAbstractItemDelegate *delegate() const override; + QString placeholderText() const override; + void setFilterWords(const QStringList &words) override; + void activate(const QModelIndex &index) override; + +private: + FileMatchDelegate *mDelegate; + QVector mMatches; }; } // namespace Tiled diff --git a/src/tiled/mainwindow.cpp b/src/tiled/mainwindow.cpp index 5b887a395f..f8de74e517 100644 --- a/src/tiled/mainwindow.cpp +++ b/src/tiled/mainwindow.cpp @@ -30,6 +30,7 @@ #include "aboutdialog.h" #include "actionmanager.h" +#include "actionsearch.h" #include "addremovetileset.h" #include "automappingmanager.h" #include "commandbutton.h" @@ -294,6 +295,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) ActionManager::registerAction(mUi->actionOffsetMap, "OffsetMap"); ActionManager::registerAction(mUi->actionOpen, "Open"); ActionManager::registerAction(mUi->actionOpenFileInProject, "OpenFileInProject"); + ActionManager::registerAction(mUi->actionSearchActions, "SearchActions"); ActionManager::registerAction(mUi->actionPaste, "Paste"); ActionManager::registerAction(mUi->actionPasteInPlace, "PasteInPlace"); ActionManager::registerAction(mUi->actionPreferences, "Preferences"); @@ -330,10 +332,12 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) QIcon saveIcon(QLatin1String(":images/16/document-save.png")); QIcon redoIcon(QLatin1String(":images/16/edit-redo.png")); QIcon undoIcon(QLatin1String(":images/16/edit-undo.png")); + QIcon searchActionsIcon(QLatin1String(":images/16/edit-find.png")); QIcon highlightCurrentLayerIcon(QLatin1String("://images/scalable/highlight-current-layer-16.svg")); openIcon.addFile(QLatin1String(":images/24/document-open.png")); saveIcon.addFile(QLatin1String(":images/24/document-save.png")); + searchActionsIcon.addFile(QLatin1String(":images/24/edit-find.png")); highlightCurrentLayerIcon.addFile(QLatin1String("://images/scalable/highlight-current-layer-24.svg")); #ifndef Q_OS_MAC @@ -344,6 +348,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) mUi->actionOpen->setIcon(openIcon); mUi->actionSave->setIcon(saveIcon); + mUi->actionSearchActions->setIcon(searchActionsIcon); QUndoGroup *undoGroup = mDocumentManager->undoGroup(); QAction *undoAction = undoGroup->createUndoAction(this, tr("Undo")); @@ -530,6 +535,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) connect(mUi->actionNewTileset, &QAction::triggered, this, [this] { newTileset(); }); connect(mUi->actionOpen, &QAction::triggered, this, &MainWindow::openFileDialog); connect(mUi->actionOpenFileInProject, &QAction::triggered, this, &MainWindow::openFileInProject); + connect(mUi->actionSearchActions, &QAction::triggered, this, &MainWindow::searchActions); connect(mUi->actionReopenClosedFile, &QAction::triggered, this, &MainWindow::reopenClosedFile); connect(mUi->actionClearRecentFiles, &QAction::triggered, preferences, &Preferences::clearRecentFiles); connect(mUi->actionSave, &QAction::triggered, this, &MainWindow::saveFile); @@ -716,6 +722,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) setThemeIcon(mUi->actionDelete, "edit-delete"); setThemeIcon(redoAction, "edit-redo"); setThemeIcon(undoAction, "edit-undo"); + setThemeIcon(mUi->actionSearchActions, "edit-find"); setThemeIcon(mUi->actionZoomIn, "zoom-in"); setThemeIcon(mUi->actionZoomOut, "zoom-out"); setThemeIcon(mUi->actionZoomNormal, "zoom-original"); @@ -785,13 +792,14 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) lockIcon.addFile(QLatin1String(":/images/24/locked.png")); mLockLayout->setIcon(lockIcon); - ActionManager::registerAction(mResetToDefaultLayout, "ResetToDefaultLayout"); - ActionManager::registerAction(mLockLayout, "LockLayout"); - mShowPropertyTypesEditor = new QAction(tr("Custom Types Editor"), this); mShowPropertyTypesEditor->setCheckable(true); - mUi->menuView->insertAction(mUi->actionShowGrid, mViewsAndToolbarsAction); + ActionManager::registerAction(mResetToDefaultLayout, "ResetToDefaultLayout"); + ActionManager::registerAction(mLockLayout, "LockLayout"); + ActionManager::registerAction(mShowPropertyTypesEditor, "CustomTypesEditor"); + + mUi->menuView->insertAction(mUi->actionSearchActions, mViewsAndToolbarsAction); mUi->menuView->insertAction(mUi->actionShowGrid, mShowPropertyTypesEditor); mUi->menuView->insertSeparator(mUi->actionShowGrid); @@ -1139,9 +1147,19 @@ void MainWindow::openFileDialog() } void MainWindow::openFileInProject() +{ + showLocatorWidget(new FileLocatorSource); +} + +void MainWindow::searchActions() +{ + showLocatorWidget(new ActionLocatorSource); +} + +void MainWindow::showLocatorWidget(LocatorSource *source) { if (mLocatorWidget) - return; + mLocatorWidget->close(); const QSize size(qMax(width() / 3, qMin(Utils::dpiScaled(600), width())), qMin(Utils::dpiScaled(600), height())); @@ -1150,7 +1168,7 @@ void MainWindow::openFileInProject() qMin(remainingHeight / 5, Utils::dpiScaled(60))); const QRect rect = QRect(mapToGlobal(localPos), size); - mLocatorWidget = new LocatorWidget(this); + mLocatorWidget = new LocatorWidget(source, this); mLocatorWidget->move(rect.topLeft()); mLocatorWidget->setMaximumSize(rect.size()); mLocatorWidget->show(); diff --git a/src/tiled/mainwindow.h b/src/tiled/mainwindow.h index 66c1395144..29bcb46776 100644 --- a/src/tiled/mainwindow.h +++ b/src/tiled/mainwindow.h @@ -52,6 +52,7 @@ class ConsoleDock; class DocumentManager; class Editor; class IssuesDock; +class LocatorSource; class LocatorWidget; class MapDocument; class MapDocumentActionHandler; @@ -117,6 +118,8 @@ class TILED_EDITOR_EXPORT MainWindow : public QMainWindow void newMap(); void openFileDialog(); void openFileInProject(); + void searchActions(); + void showLocatorWidget(LocatorSource *source); bool saveFile(); bool saveFileAs(); void saveAll(); diff --git a/src/tiled/mainwindow.ui b/src/tiled/mainwindow.ui index 9d85456470..02d6fdbbda 100644 --- a/src/tiled/mainwindow.ui +++ b/src/tiled/mainwindow.ui @@ -164,6 +164,7 @@ + @@ -792,6 +793,17 @@ Add Automapping Rules Tileset + + + Search Actions... + + + Search actions available in Tiled + + + Ctrl+Shift+P + + diff --git a/src/tiled/shapefilltool.cpp b/src/tiled/shapefilltool.cpp index abe2dcfb8e..6731b9540e 100644 --- a/src/tiled/shapefilltool.cpp +++ b/src/tiled/shapefilltool.cpp @@ -67,9 +67,23 @@ ShapeFillTool::ShapeFillTool(QObject *parent) connect(mCircleFill, &QAction::triggered, [this] { setCurrentShape(Circle); }); + setActionsEnabled(false); + ShapeFillTool::languageChanged(); } +void ShapeFillTool::activate(MapScene *scene) +{ + AbstractTileFillTool::activate(scene); + setActionsEnabled(true); +} + +void ShapeFillTool::deactivate(MapScene *scene) +{ + setActionsEnabled(false); + AbstractTileFillTool::deactivate(scene); +} + void ShapeFillTool::mousePressed(QGraphicsSceneMouseEvent *event) { // Right-click cancels drawing a shape @@ -171,6 +185,12 @@ void ShapeFillTool::updateStatusInfo() .arg(mFillBounds.width()).arg(mFillBounds.height())); } +void ShapeFillTool::setActionsEnabled(bool enabled) +{ + mRectFill->setEnabled(enabled); + mCircleFill->setEnabled(enabled); +} + void ShapeFillTool::setCurrentShape(Shape shape) { mCurrentShape = shape; diff --git a/src/tiled/shapefilltool.h b/src/tiled/shapefilltool.h index 57059c754c..0db7c5199f 100644 --- a/src/tiled/shapefilltool.h +++ b/src/tiled/shapefilltool.h @@ -33,6 +33,9 @@ class ShapeFillTool : public AbstractTileFillTool public: ShapeFillTool(QObject *parent = nullptr); + void activate(MapScene *scene) override; + void deactivate(MapScene *scene) override; + void mousePressed(QGraphicsSceneMouseEvent *event) override; void mouseReleased(QGraphicsSceneMouseEvent *event) override; @@ -67,6 +70,8 @@ class ShapeFillTool : public AbstractTileFillTool QAction *mRectFill; QAction *mCircleFill; + void setActionsEnabled(bool enabled); + void setCurrentShape(Shape shape); void updateFillOverlay(); }; diff --git a/src/tiled/stampactions.cpp b/src/tiled/stampactions.cpp index 613206c577..a8a4600cbe 100644 --- a/src/tiled/stampactions.cpp +++ b/src/tiled/stampactions.cpp @@ -74,9 +74,20 @@ StampActions::StampActions(QObject *parent) : QObject(parent) ActionManager::registerAction(mRotateLeft, "RotateLeft"); ActionManager::registerAction(mRotateRight, "RotateRight"); + setEnabled(false); languageChanged(); } +void StampActions::setEnabled(bool enabled) +{ + mRandom->setEnabled(enabled); + mWangFill->setEnabled(enabled); + mFlipHorizontal->setEnabled(enabled); + mFlipVertical->setEnabled(enabled); + mRotateLeft->setEnabled(enabled); + mRotateRight->setEnabled(enabled); +} + void StampActions::languageChanged() { mRandom->setText(tr("Random Mode")); diff --git a/src/tiled/stampactions.h b/src/tiled/stampactions.h index 06e1d7a9be..04d4c30277 100644 --- a/src/tiled/stampactions.h +++ b/src/tiled/stampactions.h @@ -34,6 +34,8 @@ class StampActions : public QObject public: StampActions(QObject *parent = nullptr); + void setEnabled(bool enabled); + void languageChanged(); void populateToolBar(QToolBar *toolBar, bool isRandom, bool isWangFill); diff --git a/src/tiled/stampbrush.cpp b/src/tiled/stampbrush.cpp index e2fb52b8cf..67d752d378 100644 --- a/src/tiled/stampbrush.cpp +++ b/src/tiled/stampbrush.cpp @@ -73,10 +73,17 @@ StampBrush::~StampBrush() { } +void StampBrush::activate(MapScene *scene) +{ + AbstractTileTool::activate(scene); + mStampActions->setEnabled(true); +} + void StampBrush::deactivate(MapScene *scene) { mBrushBehavior = Free; mCaptureStampHelper.reset(); + mStampActions->setEnabled(false); AbstractTileTool::deactivate(scene); } diff --git a/src/tiled/stampbrush.h b/src/tiled/stampbrush.h index 754496cd8b..716be8891b 100644 --- a/src/tiled/stampbrush.h +++ b/src/tiled/stampbrush.h @@ -49,6 +49,7 @@ class StampBrush : public AbstractTileTool StampBrush(QObject *parent = nullptr); ~StampBrush() override; + void activate(MapScene *scene) override; void deactivate(MapScene *scene) override; void mousePressed(QGraphicsSceneMouseEvent *event) override;