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