Skip to content

Commit 3e6b93e

Browse files
committed
Add Jump to Tile Coordinates feature
- Implement TileLocatorSource for parsing tile coordinates - Add 'Jump to Tile' action with Ctrl+Shift+G shortcut - Support multiple coordinate formats: x,y, x:10 y:20, 10 20, etc. - Update keyboard shortcuts documentation - Fixes issue #1367
1 parent f019143 commit 3e6b93e

File tree

7 files changed

+393
-0
lines changed

7 files changed

+393
-0
lines changed

docs/manual/keyboard-shortcuts.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ General
1515
- ``Ctrl + O`` - Open any file or project
1616
- ``Ctrl + P`` - Open a file in the current project
1717
- ``Ctrl + Shift + P`` - Search for available actions
18+
- ``Ctrl + Shift + G`` - Jump to tile coordinates
1819
- ``Ctrl + Shift + T`` - Reopen a recently closed file
1920
- ``Ctrl + S`` - Save current document
2021
- ``Ctrl + Shift + S`` - Save current document to another file

src/tiled/libtilededitor.qbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ DynamicLibrary {
289289
"layeroffsettool.h",
290290
"locatorwidget.cpp",
291291
"locatorwidget.h",
292+
"tilelocator.cpp",
293+
"tilelocator.h",
292294
"magicwandtool.h",
293295
"magicwandtool.cpp",
294296
"maintoolbar.cpp",

src/tiled/mainwindow.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include "issuesdock.h"
4444
#include "layer.h"
4545
#include "locatorwidget.h"
46+
#include "tilelocator.h"
4647
#include "map.h"
4748
#include "mapdocument.h"
4849
#include "mapdocumentactionhandler.h"
@@ -289,6 +290,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
289290
ActionManager::registerAction(mUi->actionOpen, "Open");
290291
ActionManager::registerAction(mUi->actionOpenFileInProject, "OpenFileInProject");
291292
ActionManager::registerAction(mUi->actionSearchActions, "SearchActions");
293+
ActionManager::registerAction(mUi->actionJumpToTile, "JumpToTile");
292294
ActionManager::registerAction(mUi->actionPaste, "Paste");
293295
ActionManager::registerAction(mUi->actionPasteInPlace, "PasteInPlace");
294296
ActionManager::registerAction(mUi->actionPreferences, "Preferences");
@@ -530,6 +532,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags)
530532
connect(mUi->actionOpen, &QAction::triggered, this, &MainWindow::openFileDialog);
531533
connect(mUi->actionOpenFileInProject, &QAction::triggered, this, &MainWindow::openFileInProject);
532534
connect(mUi->actionSearchActions, &QAction::triggered, this, &MainWindow::searchActions);
535+
connect(mUi->actionJumpToTile, &QAction::triggered, this, &MainWindow::jumpToTile);
533536
connect(mUi->actionReopenClosedFile, &QAction::triggered, this, &MainWindow::reopenClosedFile);
534537
connect(mUi->actionClearRecentFiles, &QAction::triggered, preferences, &Preferences::clearRecentFiles);
535538
connect(mUi->actionSave, &QAction::triggered, this, &MainWindow::saveFile);
@@ -1146,6 +1149,11 @@ void MainWindow::searchActions()
11461149
showLocatorWidget(new ActionLocatorSource);
11471150
}
11481151

1152+
void MainWindow::jumpToTile()
1153+
{
1154+
showLocatorWidget(new TileLocatorSource);
1155+
}
1156+
11491157
void MainWindow::showLocatorWidget(LocatorSource *source)
11501158
{
11511159
if (mLocatorWidget)

src/tiled/mainwindow.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class TILED_EDITOR_EXPORT MainWindow : public QMainWindow
117117
void openFileDialog();
118118
void openFileInProject();
119119
void searchActions();
120+
void jumpToTile();
120121
void showLocatorWidget(LocatorSource *source);
121122
bool saveFile();
122123
bool saveFileAs();

src/tiled/mainwindow.ui

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
<addaction name="actionSnapToPixels"/>
166166
</widget>
167167
<addaction name="actionSearchActions"/>
168+
<addaction name="actionJumpToTile"/>
168169
<addaction name="actionShowGrid"/>
169170
<addaction name="actionShowTileObjectOutlines"/>
170171
<addaction name="actionShowObjectReferences"/>
@@ -803,6 +804,17 @@
803804
<string notr="true">Ctrl+Shift+P</string>
804805
</property>
805806
</action>
807+
<action name="actionJumpToTile">
808+
<property name="text">
809+
<string>Jump to Tile...</string>
810+
</property>
811+
<property name="toolTip">
812+
<string>Jump to a specific tile coordinate</string>
813+
</property>
814+
<property name="shortcut">
815+
<string notr="true">Ctrl+Shift+G</string>
816+
</property>
817+
</action>
806818
<action name="actionUnloadAllWorlds">
807819
<property name="text">
808820
<string>Unload All Worlds</string>

src/tiled/tilelocator.cpp

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/*
2+
* tilelocator.cpp
3+
* Copyright 2025, Siraj Ahmadzai
4+
*
5+
* This file is part of Tiled.
6+
*
7+
* This program is free software; you can redistribute it and/or modify it
8+
* under the terms of the GNU General Public License as published by the Free
9+
* Software Foundation; either version 2 of the License, or (at your option)
10+
* any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15+
* more details.
16+
*
17+
* You should have received a copy of the GNU General Public License along with
18+
* this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
#include "tilelocator.h"
22+
23+
#include "documentmanager.h"
24+
#include "mapdocument.h"
25+
#include "mapview.h"
26+
#include "maprenderer.h"
27+
#include "layer.h"
28+
#include "utils.h"
29+
#include "locatorwidget.h"
30+
31+
#include <QApplication>
32+
#include <QPainter>
33+
#include <QRegularExpression>
34+
#include <QStyledItemDelegate>
35+
#include <QTreeView>
36+
#include <QStaticText>
37+
#include <QTextOption>
38+
39+
namespace Tiled {
40+
41+
///////////////////////////////////////////////////////////////////////////////
42+
43+
class TileMatchDelegate : public QStyledItemDelegate
44+
{
45+
public:
46+
explicit TileMatchDelegate(QObject *parent = nullptr)
47+
: QStyledItemDelegate(parent)
48+
{}
49+
50+
void paint(QPainter *painter,
51+
const QStyleOptionViewItem &option,
52+
const QModelIndex &index) const override
53+
{
54+
painter->save();
55+
56+
const QString displayText = index.data().toString();
57+
const QString description = index.data(Qt::UserRole).toString();
58+
59+
const Fonts fonts(option.font);
60+
const QFontMetrics bigFontMetrics(fonts.big);
61+
62+
const int margin = Utils::dpiScaled(2);
63+
const auto displayRect = option.rect.adjusted(margin, margin, -margin, 0);
64+
const auto descriptionRect = option.rect.adjusted(margin, margin + bigFontMetrics.lineSpacing(), -margin, 0);
65+
66+
// draw the background (covers selection)
67+
QStyle *style = QApplication::style();
68+
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter);
69+
70+
// adjust text color to state
71+
QPalette::ColorGroup cg = option.state & QStyle::State_Enabled
72+
? QPalette::Normal : QPalette::Disabled;
73+
if (cg == QPalette::Normal && !(option.state & QStyle::State_Active))
74+
cg = QPalette::Inactive;
75+
if (option.state & QStyle::State_Selected) {
76+
painter->setPen(option.palette.color(cg, QPalette::HighlightedText));
77+
} else {
78+
painter->setPen(option.palette.color(cg, QPalette::Text));
79+
}
80+
81+
QTextOption textOption;
82+
textOption.setWrapMode(QTextOption::NoWrap);
83+
84+
QStaticText staticText(displayText);
85+
staticText.setTextOption(textOption);
86+
87+
painter->setFont(fonts.big);
88+
painter->drawStaticText(displayRect.topLeft(), staticText);
89+
90+
if (!description.isEmpty()) {
91+
staticText.setText(description);
92+
painter->setOpacity(0.75);
93+
painter->setFont(fonts.small);
94+
painter->drawStaticText(descriptionRect.topLeft(), staticText);
95+
}
96+
97+
// draw the focus rect
98+
if (option.state & QStyle::State_HasFocus) {
99+
QStyleOptionFocusRect focusOption;
100+
focusOption.initFrom(option.widget);
101+
focusOption.rect = option.rect;
102+
focusOption.state = option.state;
103+
style->drawPrimitive(QStyle::PE_FrameFocusRect, &focusOption, painter);
104+
}
105+
106+
painter->restore();
107+
}
108+
109+
QSize sizeHint(const QStyleOptionViewItem &option,
110+
const QModelIndex &index) const override
111+
{
112+
const Fonts fonts(option.font);
113+
const QFontMetrics bigFontMetrics(fonts.big);
114+
const QFontMetrics smallFontMetrics(fonts.small);
115+
116+
const QString displayText = index.data().toString();
117+
const QString description = index.data(Qt::UserRole).toString();
118+
119+
const int margin = Utils::dpiScaled(2);
120+
const int height = margin * 2 + bigFontMetrics.lineSpacing();
121+
122+
if (!description.isEmpty())
123+
return QSize(option.rect.width(), height + smallFontMetrics.lineSpacing());
124+
125+
return QSize(option.rect.width(), height);
126+
}
127+
128+
private:
129+
class Fonts {
130+
public:
131+
Fonts(const QFont &base)
132+
: small(scaledFont(base, 0.9))
133+
, big(scaledFont(base, 1.1))
134+
{}
135+
136+
const QFont small;
137+
const QFont big;
138+
};
139+
140+
static QFont scaledFont(const QFont &font, qreal scale)
141+
{
142+
QFont scaled(font);
143+
if (font.pixelSize() > 0)
144+
scaled.setPixelSize(font.pixelSize() * scale);
145+
else
146+
scaled.setPointSizeF(font.pointSizeF() * scale);
147+
return scaled;
148+
}
149+
};
150+
151+
///////////////////////////////////////////////////////////////////////////////
152+
153+
TileLocatorSource::TileLocatorSource(QObject *parent)
154+
: LocatorSource(parent)
155+
, mDelegate(new TileMatchDelegate(this))
156+
{}
157+
158+
int TileLocatorSource::rowCount(const QModelIndex &parent) const
159+
{
160+
return parent.isValid() ? 0 : mMatches.size();
161+
}
162+
163+
QVariant TileLocatorSource::data(const QModelIndex &index, int role) const
164+
{
165+
switch (role) {
166+
case Qt::DisplayRole: {
167+
const Match &match = mMatches.at(index.row());
168+
return match.displayText;
169+
}
170+
case Qt::UserRole: {
171+
const Match &match = mMatches.at(index.row());
172+
return match.description;
173+
}
174+
}
175+
return QVariant();
176+
}
177+
178+
QAbstractItemDelegate *TileLocatorSource::delegate() const
179+
{
180+
return mDelegate;
181+
}
182+
183+
QString TileLocatorSource::placeholderText() const
184+
{
185+
return QCoreApplication::translate("Tiled::LocatorWidget", "Enter tile coordinates (e.g., 10,20 or x:10 y:20)");
186+
}
187+
188+
void TileLocatorSource::activate(const QModelIndex &index)
189+
{
190+
const Match &match = mMatches.at(index.row());
191+
192+
auto documentManager = DocumentManager::instance();
193+
auto mapDocument = qobject_cast<MapDocument*>(documentManager->currentDocument());
194+
195+
if (!mapDocument) {
196+
// Try to find any open map document
197+
const auto documents = documentManager->documents();
198+
for (auto document : documents) {
199+
if (auto mapDoc = qobject_cast<MapDocument*>(document.data())) {
200+
mapDocument = mapDoc;
201+
break;
202+
}
203+
}
204+
}
205+
206+
if (!mapDocument)
207+
return;
208+
209+
auto mapView = documentManager->viewForDocument(mapDocument);
210+
211+
if (!mapView)
212+
return;
213+
214+
// Convert tile coordinates to screen coordinates
215+
auto renderer = mapDocument->renderer();
216+
auto screenPos = renderer->tileToScreenCoords(match.tilePos);
217+
218+
// Center the view on the specified tile position
219+
mapView->forceCenterOn(screenPos);
220+
}
221+
222+
void TileLocatorSource::setFilterWords(const QStringList &words)
223+
{
224+
auto matches = parseCoordinates(words);
225+
226+
beginResetModel();
227+
mMatches = std::move(matches);
228+
endResetModel();
229+
}
230+
231+
QVector<TileLocatorSource::Match> TileLocatorSource::parseCoordinates(const QStringList &words)
232+
{
233+
QVector<Match> result;
234+
235+
if (words.isEmpty())
236+
return result;
237+
238+
// Join all words to handle cases like "10,20" or "x:10 y:20"
239+
QString input = words.join(QStringLiteral(" "));
240+
241+
QPoint tilePos;
242+
if (parseCoordinate(input, tilePos)) {
243+
Match match;
244+
match.tilePos = tilePos;
245+
match.displayText = QCoreApplication::translate("Tiled::TileLocatorSource", "Jump to tile (%1, %2)")
246+
.arg(tilePos.x()).arg(tilePos.y());
247+
match.description = QCoreApplication::translate("Tiled::TileLocatorSource", "Press Enter to jump to this tile");
248+
result.append(match);
249+
}
250+
251+
return result;
252+
}
253+
254+
bool TileLocatorSource::parseCoordinate(const QString &text, QPoint &tilePos)
255+
{
256+
// Try different coordinate formats
257+
258+
// Format 1: "x,y" (e.g., "10,20")
259+
QRegularExpression commaPattern(QStringLiteral(R"((\d+)\s*,\s*(\d+))"));
260+
auto match = commaPattern.match(text);
261+
if (match.hasMatch()) {
262+
tilePos.setX(match.captured(1).toInt());
263+
tilePos.setY(match.captured(2).toInt());
264+
return true;
265+
}
266+
267+
// Format 2: "x:10 y:20" or "x:10, y:20"
268+
QRegularExpression xyPattern(QStringLiteral(R"(x\s*:\s*(\d+)(?:\s*[,\s]\s*y\s*:\s*(\d+))?)"));
269+
match = xyPattern.match(text);
270+
if (match.hasMatch()) {
271+
tilePos.setX(match.captured(1).toInt());
272+
if (match.captured(2).isEmpty()) {
273+
// Only x coordinate provided, try to find y in the rest of the text
274+
QString remaining = text.mid(match.capturedEnd());
275+
QRegularExpression yPattern(QStringLiteral(R"(y\s*:\s*(\d+))"));
276+
auto yMatch = yPattern.match(remaining);
277+
if (yMatch.hasMatch()) {
278+
tilePos.setY(yMatch.captured(1).toInt());
279+
return true;
280+
}
281+
} else {
282+
tilePos.setY(match.captured(2).toInt());
283+
return true;
284+
}
285+
}
286+
287+
// Format 3: Just two numbers separated by space (e.g., "10 20")
288+
QRegularExpression spacePattern(QStringLiteral(R"((\d+)\s+(\d+))"));
289+
match = spacePattern.match(text);
290+
if (match.hasMatch()) {
291+
tilePos.setX(match.captured(1).toInt());
292+
tilePos.setY(match.captured(2).toInt());
293+
return true;
294+
}
295+
296+
// Format 4: Single number (assume it's x coordinate)
297+
QRegularExpression singlePattern(QStringLiteral(R"((\d+))"));
298+
match = singlePattern.match(text);
299+
if (match.hasMatch()) {
300+
tilePos.setX(match.captured(1).toInt());
301+
tilePos.setY(0); // Default y to 0
302+
return true;
303+
}
304+
305+
return false;
306+
}
307+
308+
} // namespace Tiled
309+
310+
#include "moc_tilelocator.cpp"

0 commit comments

Comments
 (0)