Skip to content

Commit 7dc7cfe

Browse files
dl3sdodg0yt
andauthored
MapFindFeature: Exclude hidden/protected objects (GH-2371)
The 'Find next' search failed if the object query returned an object with either a hidden or protected symbol since these kind of objects can't be added to an object selection. This change excludes such objects from the result set. A test is added. --------- Co-authored-by: Kai Pastor <[email protected]>
1 parent ee52ae6 commit 7dc7cfe

File tree

4 files changed

+130
-54
lines changed

4 files changed

+130
-54
lines changed

src/gui/map/map_find_feature.cpp

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 Kai Pastor
2+
* Copyright 2017-2020, 2024, 2025 Kai Pastor
33
*
44
* This file is part of OpenOrienteering.
55
*
@@ -22,8 +22,8 @@
2222

2323
#include <functional>
2424

25-
#include <QAction>
2625
#include <QAbstractButton>
26+
#include <QAction>
2727
#include <QDialog>
2828
#include <QDialogButtonBox>
2929
#include <QGridLayout>
@@ -35,7 +35,9 @@
3535

3636
#include "core/map.h"
3737
#include "core/map_part.h"
38+
#include "core/objects/object.h"
3839
#include "core/objects/object_query.h"
40+
#include "core/symbols/symbol.h"
3941
#include "gui/main_window.h"
4042
#include "gui/util_gui.h"
4143
#include "gui/map/map_editor.h"
@@ -44,7 +46,17 @@
4446

4547
namespace OpenOrienteering {
4648

47-
class Object;
49+
namespace {
50+
51+
// Returns true if an object can be added to the selection.
52+
bool isSelectable(const Object* object)
53+
{
54+
const auto* symbol = object ? object->getSymbol() : nullptr;
55+
return symbol && !symbol->isHidden() && !symbol->isProtected();
56+
}
57+
58+
} // namespace
59+
4860

4961
MapFindFeature::MapFindFeature(MapEditorController& controller)
5062
: QObject{nullptr}
@@ -117,7 +129,7 @@ void MapFindFeature::showDialog()
117129

118130
auto button_box = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help);
119131
connect(button_box, &QDialogButtonBox::rejected, &*find_dialog, &QDialog::hide);
120-
connect(button_box->button(QDialogButtonBox::Help), &QPushButton::clicked, this, &MapFindFeature::showHelp);
132+
connect(button_box, &QDialogButtonBox::helpRequested, this, &MapFindFeature::showHelp);
121133

122134
editor_stack = new QStackedLayout();
123135
editor_stack->addWidget(text_edit);
@@ -166,52 +178,54 @@ ObjectQuery MapFindFeature::makeQuery() const
166178
query = tag_selector->makeQuery();
167179
}
168180
}
181+
if (!query)
182+
{
183+
controller.getMap()->clearObjectSelection(true);
184+
controller.getWindow()->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("Invalid query"), 2000);
185+
}
169186
return query;
170187
}
171188

172189

173190
void MapFindFeature::findNext()
174191
{
175-
auto map = controller.getMap();
176-
auto first_object = map->getFirstSelectedObject();
192+
if (auto query = makeQuery())
193+
findNextMatchingObject(controller, query);
194+
}
195+
196+
// static
197+
void MapFindFeature::findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query)
198+
{
199+
auto* map = controller.getMap();
200+
201+
Object* first_match = nullptr; // the first match in all objects
202+
Object* pivot_object = map->getFirstSelectedObject();
203+
Object* next_match = nullptr; // the next match after pivot_object
177204
map->clearObjectSelection(false);
178205

179-
Object* next_object = nullptr;
180-
auto query = makeQuery();
181-
if (!query)
182-
{
183-
if (auto window = controller.getWindow())
184-
window->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("Invalid query"), 2000);
185-
return;
186-
}
206+
auto search = [&](Object* object) {
207+
if (next_match)
208+
return;
187209

188-
auto search = [&first_object, &next_object, &query](Object* object) {
189-
if (!next_object)
210+
bool after_pivot = (pivot_object == nullptr);
211+
if (object == pivot_object)
212+
pivot_object = nullptr;
213+
214+
if (isSelectable(object) && query(object))
190215
{
191-
if (first_object)
192-
{
193-
if (object == first_object)
194-
first_object = nullptr;
195-
}
196-
else if (query(object))
197-
{
198-
next_object = object;
199-
}
216+
if (after_pivot)
217+
next_match = object;
218+
else if (!first_match)
219+
first_match = object;
200220
}
201221
};
202222

203-
// Start from selected object
204223
map->getCurrentPart()->applyOnAllObjects(search);
205-
if (!next_object)
206-
{
207-
// Start from first object
208-
first_object = nullptr;
209-
map->getCurrentPart()->applyOnAllObjects(search);
210-
}
224+
if (!next_match)
225+
next_match = first_match;
226+
if (next_match)
227+
map->addObjectToSelection(next_match, false);
211228

212-
map->clearObjectSelection(false);
213-
if (next_object)
214-
map->addObjectToSelection(next_object, false);
215229
map->emitSelectionChanged();
216230
map->ensureVisibilityOfSelectedObjects(Map::FullVisibility);
217231

@@ -221,20 +235,22 @@ void MapFindFeature::findNext()
221235

222236

223237
void MapFindFeature::findAll()
238+
{
239+
if (auto query = makeQuery())
240+
findAllMatchingObjects(controller, query);
241+
}
242+
243+
// static
244+
void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query)
224245
{
225246
auto map = controller.getMap();
226247
map->clearObjectSelection(false);
227248

228-
auto query = makeQuery();
229-
if (!query)
230-
{
231-
controller.getWindow()->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("Invalid query"), 2000);
232-
return;
233-
}
234-
235249
map->getCurrentPart()->applyOnMatchingObjects([map](Object* object) {
236-
map->addObjectToSelection(object, false);
250+
if (isSelectable(object))
251+
map->addObjectToSelection(object, false);
237252
}, std::cref(query));
253+
238254
map->emitSelectionChanged();
239255
map->ensureVisibilityOfSelectedObjects(Map::FullVisibility);
240256
controller.getWindow()->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("%n object(s) selected", nullptr, map->getNumSelectedObjects()), 2000);
@@ -244,15 +260,12 @@ void MapFindFeature::findAll()
244260
}
245261

246262

247-
248263
void MapFindFeature::showHelp() const
249264
{
250265
Util::showHelp(controller.getWindow(), "find_objects.html");
251266
}
252267

253268

254-
255-
// slot
256269
void MapFindFeature::tagSelectorToggled(bool active)
257270
{
258271
editor_stack->setCurrentIndex(active ? 1 : 0);

src/gui/map/map_find_feature.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 Kai Pastor
2+
* Copyright 2017-2019, 2025 Kai Pastor
33
*
44
* This file is part of OpenOrienteering.
55
*
@@ -37,7 +37,6 @@ class MapEditorController;
3737
class ObjectQuery;
3838
class TagSelectWidget;
3939

40-
4140
/**
4241
* Provides an interactive feature for finding objects in the map.
4342
*
@@ -48,7 +47,6 @@ class TagSelectWidget;
4847
class MapFindFeature : public QObject
4948
{
5049
Q_OBJECT
51-
5250
public:
5351
MapFindFeature(MapEditorController& controller);
5452

@@ -60,6 +58,10 @@ class MapFindFeature : public QObject
6058

6159
QAction* findNextAction() { return find_next_action; }
6260

61+
static void findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query);
62+
63+
static void findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query);
64+
6365
private:
6466
void showDialog();
6567

@@ -73,6 +75,7 @@ class MapFindFeature : public QObject
7375

7476
void tagSelectorToggled(bool active);
7577

78+
7679
MapEditorController& controller;
7780
QPointer<QDialog> find_dialog; // child of controller's window
7881
QStackedLayout* editor_stack = nullptr; // child of find_dialog
@@ -88,4 +91,4 @@ class MapFindFeature : public QObject
8891

8992
} // namespace OpenOrienteering
9093

91-
#endif
94+
#endif // OPENORIENTEERING_MAP_FIND_FEATURE_H

test/tools_t.cpp

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Copyright 2012, 2013 Thomas Schöps
3-
* Copyright 2015-2020 Kai Pastor
3+
* Copyright 2015-2020, 2025 Kai Pastor
4+
* Copyright 2025 Matthias Kühlewein
45
*
56
* This file is part of OpenOrienteering.
67
*
@@ -35,10 +36,13 @@
3536
#include "core/map_color.h"
3637
#include "core/map_coord.h"
3738
#include "core/objects/object.h"
39+
#include "core/objects/object_query.h"
3840
#include "core/symbols/line_symbol.h"
41+
#include "core/symbols/point_symbol.h"
3942
#include "global.h"
4043
#include "gui/main_window.h"
4144
#include "gui/map/map_editor.h"
45+
#include "gui/map/map_find_feature.h"
4246
#include "gui/map/map_widget.h"
4347
#include "templates/paint_on_template_feature.h"
4448
#include "tools/edit_point_tool.h"
@@ -161,7 +165,7 @@ void TestMapEditor::simulateDrag(const QPointF& start_pos, const QPointF& end_po
161165
}
162166

163167

164-
// ### TestTools ###
168+
// ### ToolsTest ###
165169

166170
void ToolsTest::initTestCase()
167171
{
@@ -229,14 +233,68 @@ void ToolsTest::paintOnTemplateFeature()
229233
}
230234

231235

236+
void ToolsTest::testFindObjects()
237+
{
238+
auto* map = new Map;
239+
{
240+
auto* normal_point_symbol = new PointSymbol();
241+
map->addSymbol(normal_point_symbol, 0);
242+
243+
auto* hidden_point_symbol = new PointSymbol();
244+
hidden_point_symbol->setHidden(true);
245+
map->addSymbol(hidden_point_symbol, 1);
246+
247+
auto* protected_point_symbol = new PointSymbol();
248+
protected_point_symbol->setProtected(true);
249+
map->addSymbol(protected_point_symbol, 2);
250+
251+
auto add_object = [map](Symbol* symbol, const char* label) {
252+
auto* object = new PointObject(symbol);
253+
object->setTag(QLatin1String("match"), QLatin1String(label));
254+
map->addObject(object);
255+
};
256+
add_object(normal_point_symbol, "yes"); // expected match
257+
add_object(normal_point_symbol, "no");
258+
add_object(normal_point_symbol, "yes"); // expected match
259+
add_object(hidden_point_symbol, "yes");
260+
add_object(normal_point_symbol, "yes"); // expected match
261+
add_object(protected_point_symbol, "yes");
262+
}
263+
264+
TestMapEditor editor(map); // taking ownership
265+
266+
ObjectQuery query {QLatin1String("match"), ObjectQuery::OperatorIs, QLatin1String("yes")};
267+
QVERIFY(query);
268+
269+
MapFindFeature::findAllMatchingObjects(*editor.editor, query);
270+
QCOMPARE(map->getNumSelectedObjects(), 3);
271+
272+
MapFindFeature::findNextMatchingObject(*editor.editor, query);
273+
QCOMPARE(map->getNumSelectedObjects(), 1);
274+
auto* first_match = map->getFirstSelectedObject();
275+
276+
MapFindFeature::findNextMatchingObject(*editor.editor, query);
277+
QCOMPARE(map->getNumSelectedObjects(), 1);
278+
QVERIFY(map->getFirstSelectedObject() != first_match);
279+
280+
MapFindFeature::findNextMatchingObject(*editor.editor, query);
281+
QCOMPARE(map->getNumSelectedObjects(), 1);
282+
QVERIFY(map->getFirstSelectedObject() != first_match);
283+
284+
MapFindFeature::findNextMatchingObject(*editor.editor, query);
285+
QCOMPARE(map->getNumSelectedObjects(), 1);
286+
QVERIFY(map->getFirstSelectedObject() == first_match);
287+
}
288+
289+
232290
/*
233291
* We select a non-standard QPA because we don't need a real GUI window.
234292
*
235293
* Normally, the "offscreen" plugin would be the correct one.
236294
* However, it bails out with a QFontDatabase error (cf. QTBUG-33674)
237295
*/
238296
namespace {
239-
auto Q_DECL_UNUSED qpa_selected = qputenv("QT_QPA_PLATFORM", "minimal"); // clazy:exclude=non-pod-global-static
297+
auto const Q_DECL_UNUSED qpa_selected = qputenv("QT_QPA_PLATFORM", "minimal"); // clazy:exclude=non-pod-global-static
240298
}
241299

242300

test/tools_t.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Copyright 2012, 2013 Thomas Schöps
3-
* Copyright 2017 Kai Pastor
3+
* Copyright 2017, 2020, 2025 Kai Pastor
44
*
55
* This file is part of OpenOrienteering.
66
*
@@ -36,6 +36,8 @@ private slots:
3636
void editTool();
3737

3838
void paintOnTemplateFeature();
39+
40+
void testFindObjects();
3941
};
4042

4143
#endif

0 commit comments

Comments
 (0)