Skip to content

Commit 88e57b6

Browse files
committed
Feature: Remove object tags
Add dialog to remove object tags from selected objects that match a configurable pattern. Allow to create an undo step. Closes GH-2354 (Remove objects tags).
1 parent d5f8dea commit 88e57b6

File tree

6 files changed

+282
-4
lines changed

6 files changed

+282
-4
lines changed

code-check-wrapper.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh -e
22

3-
# Copyright 2017, 2018, 2024 Kai Pastor
3+
# Copyright 2017, 2018, 2024, 2025 Kai Pastor
44
#
55
# This file is part of OpenOrienteering.
66
#
@@ -103,6 +103,7 @@ for I in \
103103
symbol_rule_set.cpp \
104104
symbol_t.cpp \
105105
symbol_tooltip.cpp \
106+
tag_remove_dialog.cpp \
106107
tag_select_widget.cpp \
107108
/template.cpp \
108109
template_image.cpp \

src/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# Copyright 2012-2014 Thomas Schöps
3-
# Copyright 2012-2024 Kai Pastor
3+
# Copyright 2012-2025 Kai Pastor
44
#
55
# This file is part of OpenOrienteering.
66
#
@@ -154,6 +154,7 @@ set(Mapper_Common_SRCS
154154
gui/select_crs_dialog.cpp
155155
gui/settings_dialog.cpp
156156
gui/simple_course_dialog.cpp
157+
gui/tag_remove_dialog.cpp
157158
gui/task_dialog.cpp
158159
gui/text_browser_dialog.cpp
159160
gui/touch_cursor.cpp

src/gui/map/map_editor.cpp

Lines changed: 13 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 2012-2021, 2024 Kai Pastor
3+
* Copyright 2012-2021, 2024, 2025 Kai Pastor
44
*
55
* This file is part of OpenOrienteering.
66
*
@@ -116,6 +116,7 @@
116116
#include "gui/main_window.h"
117117
#include "gui/print_widget.h"
118118
#include "gui/simple_course_dialog.h"
119+
#include "gui/tag_remove_dialog.h"
119120
#include "gui/text_browser_dialog.h"
120121
#include "gui/util_gui.h"
121122
#include "gui/map/map_dialog_scale.h"
@@ -1031,6 +1032,7 @@ void MapEditorController::createActions()
10311032
rotate_map_act = newAction("rotatemap", tr("Rotate map..."), this, SLOT(rotateMapClicked()), "tool-rotate.png", tr("Rotate the whole map"), "map_menu.html");
10321033
map_notes_act = newAction("mapnotes", tr("Map notes..."), this, SLOT(mapNotesClicked()), nullptr, QString{}, "map_menu.html");
10331034
map_info_act = newAction("mapinfo", tr("Map information..."), this, SLOT(mapInfoClicked()), "map-information.png", QString{}, "map_menu.html");
1035+
cleanup_tags_act = newAction("cleanuptags", tr("Cleanup object tags..."), this, SLOT(cleanupObjectTagsClicked()), "delete.png", QString{}, "map_menu.html");
10341036

10351037
template_window_act = newCheckAction("templatewindow", tr("Template setup window"), this, SLOT(showTemplateWindow(bool)), "templates.png", tr("Show/Hide the template window"), "templates_menu.html");
10361038
//QAction* template_config_window_act = newCheckAction("templateconfigwindow", tr("Template configurations window"), this, SLOT(showTemplateConfigurationsWindow(bool)), "window-new", tr("Show/Hide the template configurations window"));
@@ -1259,6 +1261,7 @@ void MapEditorController::createMenuAndToolbars()
12591261
map_menu->addAction(rotate_map_act);
12601262
map_menu->addAction(map_notes_act);
12611263
map_menu->addAction(map_info_act);
1264+
map_menu->addAction(cleanup_tags_act);
12621265
map_menu->addSeparator();
12631266
updateMapPartsUI();
12641267
map_menu->addAction(mappart_add_act);
@@ -2281,6 +2284,13 @@ void MapEditorController::mapInfoClicked()
22812284
dialog.exec();
22822285
}
22832286

2287+
void MapEditorController::cleanupObjectTagsClicked()
2288+
{
2289+
TagRemoveDialog dialog(window, map);
2290+
dialog.setWindowModality(Qt::WindowModal);
2291+
dialog.exec();
2292+
}
2293+
22842294
void MapEditorController::createTemplateWindow()
22852295
{
22862296
Q_ASSERT(!template_dock_widget);
@@ -2611,6 +2621,8 @@ void MapEditorController::updateObjectDependentActions()
26112621
scale_act->setEnabled(have_selection);
26122622
scale_act->setStatusTip(tr("Scale the selected objects.") + (scale_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
26132623
mappart_move_menu->setEnabled(have_selection && have_multiple_parts);
2624+
cleanup_tags_act->setEnabled(have_selection);
2625+
cleanup_tags_act->setStatusTip(tr("Remove tags from the selected objects.") + (cleanup_tags_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
26142626

26152627
// have_rotatable_pattern || have_rotatable_point
26162628
rotate_pattern_act->setEnabled(have_rotatable_pattern || have_rotatable_object);

src/gui/map/map_editor.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Copyright 2012, 2013, 2014 Thomas Schöps
3-
* Copyright 2013-2024 Kai Pastor
3+
* Copyright 2013-2025 Kai Pastor
44
*
55
* This file is part of OpenOrienteering.
66
*
@@ -352,6 +352,8 @@ public slots:
352352
void mapNotesClicked();
353353
/** Shows the map information. */
354354
void mapInfoClicked();
355+
/** Shows the TagRemoveDialog. */
356+
void cleanupObjectTagsClicked();
355357

356358
/** Shows or hides the template setup dock widget. */
357359
void showTemplateWindow(bool show);
@@ -752,6 +754,7 @@ protected slots:
752754
QAction* rotate_map_act = {};
753755
QAction* map_notes_act = {};
754756
QAction* map_info_act = {};
757+
QAction* cleanup_tags_act = {};
755758
QAction* symbol_set_id_act = {};
756759
std::unique_ptr<SymbolReportFeature> symbol_report_feature;
757760

src/gui/tag_remove_dialog.cpp

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2025 Matthias Kühlewein
3+
*
4+
* This file is part of OpenOrienteering.
5+
*
6+
* OpenOrienteering is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* OpenOrienteering is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#include "tag_remove_dialog.h"
21+
22+
#include <algorithm>
23+
#include <functional>
24+
#include <set>
25+
#include <vector>
26+
27+
#include <Qt>
28+
#include <QtGlobal>
29+
#include <QChar>
30+
#include <QCheckBox>
31+
#include <QComboBox>
32+
#include <QCoreApplication>
33+
#include <QDialogButtonBox>
34+
#include <QHBoxLayout>
35+
#include <QLabel>
36+
#include <QLatin1String>
37+
#include <QLineEdit>
38+
#include <QMessageBox>
39+
#include <QPushButton>
40+
#include <QVBoxLayout>
41+
#include <QWidget>
42+
43+
#include "core/map.h"
44+
#include "core/objects/object.h"
45+
#include "gui/util_gui.h"
46+
#include "undo/object_undo.h"
47+
48+
49+
namespace {
50+
51+
struct CompOpStruct {
52+
const QString op;
53+
const std::function<bool (const QString&, const QString&)> fn;
54+
};
55+
56+
static const CompOpStruct compare_operations[4] = {
57+
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "equal to"), [](const QString& key, const QString& pattern) { return key == pattern; } },
58+
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "not equal to"), [](const QString& key, const QString& pattern) { return key != pattern; } },
59+
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "containing"), [](const QString& key, const QString& pattern) { return key.contains(pattern); } },
60+
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "not containing"), [](const QString& key, const QString& pattern) { return !key.contains(pattern); } }
61+
};
62+
63+
} // namespace
64+
65+
66+
namespace OpenOrienteering {
67+
68+
TagRemoveDialog::TagRemoveDialog(QWidget* parent, Map* map)
69+
: QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint)
70+
, map { map }
71+
{
72+
setWindowTitle(tr("Remove Tags"));
73+
74+
auto* h_layout = new QHBoxLayout();
75+
h_layout->addWidget(new QLabel(tr("Remove all tags")));
76+
77+
compare_op = new QComboBox();
78+
for (auto& compare_operation : compare_operations)
79+
{
80+
compare_op->addItem(compare_operation.op);
81+
}
82+
h_layout->addWidget(compare_op);
83+
84+
pattern_edit = new QLineEdit();
85+
86+
undo_check = new QCheckBox(tr("Add undo step"));
87+
88+
auto* button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
89+
ok_button = button_box->button(QDialogButtonBox::Ok);
90+
ok_button->setEnabled(false);
91+
92+
auto* layout = new QVBoxLayout();
93+
layout->addLayout(h_layout);
94+
layout->addWidget(pattern_edit);
95+
layout->addWidget(new QLabel(tr("from the selected objects.")));
96+
layout->addWidget(undo_check);
97+
layout->addItem(Util::SpacerItem::create(this));
98+
layout->addStretch();
99+
layout->addWidget(button_box);
100+
setLayout(layout);
101+
102+
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
103+
connect(button_box, &QDialogButtonBox::accepted, this, &TagRemoveDialog::okClicked);
104+
connect(pattern_edit, &QLineEdit::textChanged, this, &TagRemoveDialog::textChanged);
105+
}
106+
107+
TagRemoveDialog::~TagRemoveDialog() = default;
108+
109+
110+
// slot
111+
void TagRemoveDialog::textChanged(const QString& text)
112+
{
113+
ok_button->setEnabled(!text.trimmed().isEmpty());
114+
}
115+
116+
// slot
117+
void TagRemoveDialog::okClicked()
118+
{
119+
const auto pattern = pattern_edit->text();
120+
const auto op = compare_op->currentIndex();
121+
122+
int objects_count = 0;
123+
std::set<QString> matching_keys;
124+
125+
for (const auto& object : map->selectedObjects())
126+
{
127+
auto object_matched = false;
128+
for (const auto& tag : object->tags())
129+
{
130+
if ((compare_operations[op].fn)(tag.key, pattern))
131+
{
132+
matching_keys.insert(tag.key);
133+
object_matched = true;
134+
}
135+
}
136+
if (object_matched)
137+
++objects_count;
138+
}
139+
if (matching_keys.empty())
140+
{
141+
QMessageBox::information(this, tr("Information"), tr("No matching object tags found."), QMessageBox::Ok);
142+
return;
143+
}
144+
else
145+
{
146+
QString detailed_text = std::accumulate( begin(matching_keys),
147+
end(matching_keys),
148+
QString(tr("The following object tags will be removed:")),
149+
[](const QString& a, const QString& b) -> QString { return a + QChar::LineFeed + b; }
150+
);
151+
QMessageBox msgBox;
152+
msgBox.setWindowTitle(tr("Confirmation"));
153+
msgBox.setIcon(QMessageBox::Warning);
154+
QString question = tr("Do you want to remove %n tag(s)", nullptr, matching_keys.size());
155+
question += QChar::Space + tr("from %n object(s)?", nullptr, objects_count);
156+
msgBox.setText(question);
157+
msgBox.setDetailedText(detailed_text);
158+
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
159+
160+
if (msgBox.exec() == QMessageBox::Yes)
161+
{
162+
const auto add_undo = undo_check->isChecked();
163+
CombinedUndoStep* combined_step;
164+
if (add_undo)
165+
combined_step = new CombinedUndoStep(map);
166+
std::vector<QString> matching_keys;
167+
for (const auto& object : map->selectedObjects())
168+
{
169+
matching_keys.clear();
170+
for (const auto& tag : object->tags())
171+
{
172+
if ((compare_operations[op].fn)(tag.key, pattern))
173+
{
174+
matching_keys.push_back(tag.key);
175+
}
176+
}
177+
if (add_undo && !matching_keys.empty())
178+
{
179+
auto undo_step = new ObjectTagsUndoStep(map);
180+
undo_step->addObject(map->getCurrentPart()->findObjectIndex(object));
181+
combined_step->push(undo_step);
182+
}
183+
for (const auto& key : matching_keys)
184+
{
185+
object->removeTag(key);
186+
}
187+
}
188+
if (add_undo)
189+
map->push(combined_step);
190+
}
191+
}
192+
accept();
193+
}
194+
195+
196+
} // namespace OpenOrienteering

src/gui/tag_remove_dialog.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2025 Matthias Kühlewein
3+
*
4+
* This file is part of OpenOrienteering.
5+
*
6+
* OpenOrienteering is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* OpenOrienteering is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#ifndef OPENORIENTEERING_TAG_REMOVE_DIALOG_H
21+
#define OPENORIENTEERING_TAG_REMOVE_DIALOG_H
22+
23+
#include <QDialog>
24+
#include <QObject>
25+
26+
class QCheckBox;
27+
class QComboBox;
28+
class QLineEdit;
29+
class QPushButton;
30+
class QString;
31+
class QWidget;
32+
33+
34+
namespace OpenOrienteering {
35+
36+
class Map;
37+
38+
class TagRemoveDialog : public QDialog
39+
{
40+
Q_OBJECT
41+
public:
42+
/**
43+
* Creates a new TagRemoveDialog object.
44+
*/
45+
TagRemoveDialog(QWidget* parent, Map* map);
46+
47+
~TagRemoveDialog() override;
48+
49+
private slots:
50+
void textChanged(const QString& text);
51+
void okClicked();
52+
53+
private:
54+
QPushButton* ok_button;
55+
QCheckBox* undo_check;
56+
QComboBox* compare_op;
57+
QLineEdit* pattern_edit;
58+
59+
Map *map;
60+
};
61+
62+
63+
} // namespace OpenOrienteering
64+
65+
#endif // OPENORIENTEERING_TAG_REMOVE_DIALOG_H

0 commit comments

Comments
 (0)