Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion code-check-wrapper.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/sh -e

# Copyright 2017, 2018, 2024 Kai Pastor
# Copyright 2017, 2018, 2024, 2025 Kai Pastor
#
# This file is part of OpenOrienteering.
#
Expand Down Expand Up @@ -103,6 +103,7 @@ for I in \
symbol_rule_set.cpp \
symbol_t.cpp \
symbol_tooltip.cpp \
tag_remove_dialog.cpp \
tag_select_widget.cpp \
/template.cpp \
template_image.cpp \
Expand Down
3 changes: 2 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# Copyright 2012-2014 Thomas Schöps
# Copyright 2012-2024 Kai Pastor
# Copyright 2012-2025 Kai Pastor
#
# This file is part of OpenOrienteering.
#
Expand Down Expand Up @@ -154,6 +154,7 @@ set(Mapper_Common_SRCS
gui/select_crs_dialog.cpp
gui/settings_dialog.cpp
gui/simple_course_dialog.cpp
gui/tag_remove_dialog.cpp
gui/task_dialog.cpp
gui/text_browser_dialog.cpp
gui/touch_cursor.cpp
Expand Down
14 changes: 13 additions & 1 deletion src/gui/map/map_editor.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright 2012, 2013 Thomas Schöps
* Copyright 2012-2021, 2024 Kai Pastor
* Copyright 2012-2021, 2024, 2025 Kai Pastor
*
* This file is part of OpenOrienteering.
*
Expand Down Expand Up @@ -116,6 +116,7 @@
#include "gui/main_window.h"
#include "gui/print_widget.h"
#include "gui/simple_course_dialog.h"
#include "gui/tag_remove_dialog.h"
#include "gui/text_browser_dialog.h"
#include "gui/util_gui.h"
#include "gui/map/map_dialog_scale.h"
Expand Down Expand Up @@ -1031,6 +1032,7 @@ void MapEditorController::createActions()
rotate_map_act = newAction("rotatemap", tr("Rotate map..."), this, SLOT(rotateMapClicked()), "tool-rotate.png", tr("Rotate the whole map"), "map_menu.html");
map_notes_act = newAction("mapnotes", tr("Map notes..."), this, SLOT(mapNotesClicked()), nullptr, QString{}, "map_menu.html");
map_info_act = newAction("mapinfo", tr("Map information..."), this, SLOT(mapInfoClicked()), "map-information.png", QString{}, "map_menu.html");
cleanup_tags_act = newAction("cleanuptags", tr("Cleanup object tags..."), this, SLOT(cleanupObjectTagsClicked()), "delete.png", QString{}, "map_menu.html");

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

void MapEditorController::cleanupObjectTagsClicked()
{
TagRemoveDialog dialog(window, map);
dialog.setWindowModality(Qt::WindowModal);
dialog.exec();
}

void MapEditorController::createTemplateWindow()
{
Q_ASSERT(!template_dock_widget);
Expand Down Expand Up @@ -2611,6 +2621,8 @@ void MapEditorController::updateObjectDependentActions()
scale_act->setEnabled(have_selection);
scale_act->setStatusTip(tr("Scale the selected objects.") + (scale_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one object to activate this tool."))));
mappart_move_menu->setEnabled(have_selection && have_multiple_parts);
cleanup_tags_act->setEnabled(have_selection);
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."))));

// have_rotatable_pattern || have_rotatable_point
rotate_pattern_act->setEnabled(have_rotatable_pattern || have_rotatable_object);
Expand Down
5 changes: 4 additions & 1 deletion src/gui/map/map_editor.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright 2012, 2013, 2014 Thomas Schöps
* Copyright 2013-2024 Kai Pastor
* Copyright 2013-2025 Kai Pastor
*
* This file is part of OpenOrienteering.
*
Expand Down Expand Up @@ -352,6 +352,8 @@ public slots:
void mapNotesClicked();
/** Shows the map information. */
void mapInfoClicked();
/** Shows the TagRemoveDialog. */
void cleanupObjectTagsClicked();

/** Shows or hides the template setup dock widget. */
void showTemplateWindow(bool show);
Expand Down Expand Up @@ -752,6 +754,7 @@ protected slots:
QAction* rotate_map_act = {};
QAction* map_notes_act = {};
QAction* map_info_act = {};
QAction* cleanup_tags_act = {};
QAction* symbol_set_id_act = {};
std::unique_ptr<SymbolReportFeature> symbol_report_feature;

Expand Down
237 changes: 237 additions & 0 deletions src/gui/tag_remove_dialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright 2025 Matthias Kühlewein
*
* This file is part of OpenOrienteering.
*
* OpenOrienteering is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenOrienteering is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
*/

#include "tag_remove_dialog.h"

#include <algorithm>
#include <functional>
#include <vector>

#include <Qt>
#include <QtGlobal>
#include <QAbstractButton>
#include <QChar>
#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QLatin1String>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

#include "core/map.h"
#include "core/objects/object.h"
#include "gui/util_gui.h"
#include "undo/object_undo.h"


namespace {

struct CompOpStruct {
const QString op;
const std::function<bool (const QString&, const QString&)> fn;
};

static const CompOpStruct compare_operations[4] = {
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "is"), [](const QString& key, const QString& pattern) { return key == pattern; } },
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "is not"), [](const QString& key, const QString& pattern) { return key != pattern; } },
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "contains"), [](const QString& key, const QString& pattern) { return key.contains(pattern); } },
{ QCoreApplication::translate("OpenOrienteering::TagRemoveDialog", "contains not"), [](const QString& key, const QString& pattern) { return !key.contains(pattern); } }
};

} // namespace


namespace OpenOrienteering {

TagRemoveDialog::TagRemoveDialog(QWidget* parent, Map* map)
: QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint)
, map { map }
{
setWindowTitle(tr("Remove Tags"));

auto* search_operation_layout = new QHBoxLayout();
search_operation_layout->addWidget(new QLabel(tr("Key")));
compare_op = new QComboBox();
for (auto& compare_operation : compare_operations)
{
compare_op->addItem(compare_operation.op);
}
search_operation_layout->addWidget(compare_op);
pattern_edit = new QLineEdit();
search_operation_layout->addWidget(pattern_edit);

undo_check = new QCheckBox(tr("Add undo step"));
number_matching_objects = new QLabel();
number_matching_keys = new QLabel();
matching_keys_details = new QPlainTextEdit();
matching_keys_details->setReadOnly(true);

auto* button_box = new QDialogButtonBox();
find_button = new QPushButton(tr("Find"));
find_button->setEnabled(false);
button_box->addButton(find_button, QDialogButtonBox::ActionRole);
button_box->addButton(QDialogButtonBox::Cancel);
remove_button = new QPushButton(QIcon(QLatin1String(":/images/delete.png")), tr("Remove"));
remove_button->setEnabled(false);
button_box->addButton(remove_button, QDialogButtonBox::ActionRole);

auto* layout = new QVBoxLayout();
layout->addWidget(new QLabel(tr("Remove tags from %n selected object(s)", nullptr, map->getNumSelectedObjects())));
layout->addLayout(search_operation_layout);
layout->addWidget(undo_check);
layout->addItem(Util::SpacerItem::create(this));
layout->addWidget(number_matching_objects);
layout->addWidget(number_matching_keys);
layout->addWidget(matching_keys_details);
layout->addWidget(button_box);
setLayout(layout);

connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(find_button, &QAbstractButton::clicked, this, &TagRemoveDialog::findClicked);
connect(remove_button, &QAbstractButton::clicked, this, &TagRemoveDialog::removeClicked);
connect(pattern_edit, &QLineEdit::textChanged, this, &TagRemoveDialog::textChanged);
connect(compare_op, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &TagRemoveDialog::comboBoxChanged);
}

TagRemoveDialog::~TagRemoveDialog() = default;


// slot
void TagRemoveDialog::textChanged(const QString& text)
{
find_button->setEnabled(!text.trimmed().isEmpty());
reset();
}

// slot
void TagRemoveDialog::comboBoxChanged()
{
reset();
}

void TagRemoveDialog::reset()
{
number_matching_objects->setText(QString());
number_matching_keys->setText(QString());
matching_keys_details->clear();
remove_button->setEnabled(false);
}

// slot
void TagRemoveDialog::findClicked()
{
std::set<QString> matching_keys;
const auto objects_count = findMatchingTags(map, pattern_edit->text(), compare_op->currentIndex(), matching_keys);

number_matching_objects->setText(tr("Number of matching objects: %1").arg(objects_count));
number_matching_keys->setText(tr("%n matching key(s):", nullptr, matching_keys.size()));
matching_keys_details->clear();
remove_button->setEnabled(!matching_keys.empty());

if (!matching_keys.empty())
{
QString details = std::accumulate(begin(matching_keys),
end(matching_keys),
QString(),
[](const QString& a, const QString& b) -> QString { return a.isEmpty() ? b : a + QChar::LineFeed + b; }
);
matching_keys_details->insertPlainText(details);
}
}

// static
int TagRemoveDialog::findMatchingTags(const Map *map, const QString& pattern, int op, std::set<QString>& matching_keys)
{
int objects_count = 0;
matching_keys.clear();

for (const auto& object : map->selectedObjects())
{
auto object_matched = false;
for (const auto& tag : object->tags())
{
if ((compare_operations[op].fn)(tag.key, pattern))
{
matching_keys.insert(tag.key);
object_matched = true;
}
}
if (object_matched)
++objects_count;
}
return objects_count;
}

// slot
void TagRemoveDialog::removeClicked()
{
const auto add_undo = undo_check->isChecked();

auto question = QString(tr("Do you really want to remove the found object tags?"));
if (!add_undo)
question += QChar::LineFeed + QString(tr("This cannot be undone."));
if (QMessageBox::question(this, tr("Remove object tags"), question, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)
{
removeMatchingTags(map, pattern_edit->text(), compare_op->currentIndex(), add_undo);
accept();
}
}

// static
void TagRemoveDialog::removeMatchingTags(Map *map, const QString& pattern, int op, bool add_undo)
{
CombinedUndoStep* combined_step;
if (add_undo)
combined_step = new CombinedUndoStep(map);

std::vector<QString> matching_keys;
for (const auto& object : map->selectedObjects())
{
matching_keys.clear();
for (const auto& tag : object->tags())
{
if ((compare_operations[op].fn)(tag.key, pattern))
{
matching_keys.push_back(tag.key);
}
}
if (add_undo && !matching_keys.empty())
{
auto undo_step = new ObjectTagsUndoStep(map);
undo_step->addObject(map->getCurrentPart()->findObjectIndex(object));
combined_step->push(undo_step);
}
for (const auto& key : matching_keys)
{
object->removeTag(key);
}
}
if (add_undo)
map->push(combined_step);
}

} // namespace OpenOrienteering
Loading
Loading