Skip to content

Commit 7abc4b2

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 69da0d0 commit 7abc4b2

File tree

6 files changed

+294
-5
lines changed

6 files changed

+294
-5
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_widget.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
#
@@ -204,6 +204,7 @@ set(Mapper_Common_SRCS
204204
gui/widgets/symbol_render_widget.cpp
205205
gui/widgets/symbol_tooltip.cpp
206206
gui/widgets/symbol_widget.cpp
207+
gui/widgets/tag_remove_widget.cpp
207208
gui/widgets/tag_select_widget.cpp
208209
gui/widgets/tags_widget.cpp
209210
gui/widgets/template_list_widget.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_widget.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 <QCoreApplication>
32+
#include <QComboBox>
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+
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+
bool 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
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_WIDGET_H
21+
#define OPENORIENTEERING_TAG_REMOVE_WIDGET_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_WIDGET_H

src/gui/widgets/tags_widget.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "gui/main_window.h"
3333
#include "gui/util_gui.h"
3434
#include "gui/map/map_editor.h"
35+
#include "gui/widgets/tag_remove_widget.h"
3536
#include "undo/object_undo.h"
3637

3738

@@ -65,6 +66,9 @@ TagsWidget::TagsWidget(Map* map, MapView* main_view, MapEditorController* contro
6566
auto help_button = Util::ToolButton::create(QIcon(QString::fromLatin1(":/images/help.png")), tr("Help"));
6667
help_button->setAutoRaise(true);
6768

69+
remove_button = Util::ToolButton::create(QIcon(QString::fromLatin1(":/images/delete.png")), tr("Remove tags"));
70+
remove_button->setAutoRaise(true);
71+
6872
auto all_buttons_layout = new QHBoxLayout();
6973
QStyleOption style_option(QStyleOption::Version, QStyleOption::SO_DockWidget);
7074
all_buttons_layout->setContentsMargins(
@@ -74,6 +78,7 @@ TagsWidget::TagsWidget(Map* map, MapView* main_view, MapEditorController* contro
7478
style()->pixelMetric(QStyle::PM_LayoutBottomMargin, &style_option) / 2
7579
);
7680
all_buttons_layout->addWidget(new QLabel(QString::fromLatin1(" ")), 1);
81+
all_buttons_layout->addWidget(remove_button);
7782
all_buttons_layout->addWidget(help_button);
7883

7984
layout->addLayout(all_buttons_layout);
@@ -83,6 +88,7 @@ TagsWidget::TagsWidget(Map* map, MapView* main_view, MapEditorController* contro
8388
connect(tags_table, &QTableWidget::cellChanged, this, &TagsWidget::cellChange);
8489

8590
connect(help_button, &QAbstractButton::clicked, this, &TagsWidget::showHelp);
91+
connect(remove_button, &QAbstractButton::clicked, this, &TagsWidget::removeTags);
8692

8793
connect(map, &Map::objectSelectionChanged, this, &TagsWidget::objectTagsChanged);
8894
connect(map, &Map::selectedObjectEdited, this, &TagsWidget::objectTagsChanged);
@@ -100,6 +106,18 @@ void TagsWidget::showHelp()
100106
Util::showHelp(controller->getWindow(), "object_tags.html");
101107
}
102108

109+
// slot
110+
void TagsWidget::removeTags()
111+
{
112+
if (map)
113+
{
114+
TagRemoveDialog dialog(controller->getWindow(), map);
115+
dialog.setWindowModality(Qt::WindowModal);
116+
dialog.exec();
117+
objectTagsChanged();
118+
}
119+
}
120+
103121
void TagsWidget::setupLastRow()
104122
{
105123
const int row = tags_table->rowCount() - 1;
@@ -121,6 +139,8 @@ void TagsWidget::createUndoStep(Object* object)
121139
// slot
122140
void TagsWidget::objectTagsChanged()
123141
{
142+
remove_button->setEnabled(map->getNumSelectedObjects() > 0);
143+
124144
if (!react_to_changes)
125145
return;
126146

src/gui/widgets/tags_widget.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013 Kai Pastor
2+
* Copyright 2013-2018, 2025 Kai Pastor
33
*
44
* This file is part of OpenOrienteering.
55
*
@@ -38,7 +38,7 @@ class Object;
3838

3939

4040
/**
41-
* This widget allows for display and editing of tags, i.e. key-value pairs.
41+
* This widget allows to display and edit tags, i.e. key-value pairs.
4242
*/
4343
class TagsWidget : public QWidget
4444
{
@@ -70,6 +70,11 @@ protected slots:
7070
*/
7171
void showHelp();
7272

73+
/**
74+
* Opens a dialog for removing tags.
75+
*/
76+
void removeTags();
77+
7378
protected:
7479
/**
7580
* Sets up the last row: blank cells, right one not editable.
@@ -89,10 +94,11 @@ protected slots:
8994
MapEditorController* controller;
9095
bool react_to_changes;
9196

97+
QToolButton* remove_button;
9298
QTableWidget* tags_table;
9399
};
94100

95101

96102
} // namespace OpenOrienteering
97103

98-
#endif // TAGS_WIDGET_H
104+
#endif // OPENORIENTEERING_TAGS_WIDGET_H

0 commit comments

Comments
 (0)