[graphics/kphotoalbum] /: Support annotating images from the viewer

Jesper K. Pedersen null at kde.org
Sun Jul 30 19:36:22 BST 2023


Git commit 3a130ed45089c1b043bea8834a23be0438b4ee17 by Jesper K. Pedersen.
Committed on 30/07/2023 at 20:36.
Pushed by blackie into branch 'master'.

Support annotating images from the viewer

M  +4    -0    .reuse/dep5
M  +2    -1    CHANGELOG.md
M  +17   -4    CMakeLists.txt
M  +12   -0    DB/CategoryCollection.cpp
M  +9    -1    DB/CategoryCollection.h
A  +74   -0    DB/GlobalCategorySortOrder.cpp     [License: GPL(v2.0+)]
A  +49   -0    DB/GlobalCategorySortOrder.h     [License: GPL(v2.0+)]
M  +1    -1    DB/ImageDB.cpp
M  +17   -0    DB/XML/FileReader.cpp
M  +1    -0    DB/XML/FileReader.h
M  +11   -0    DB/XML/FileWriter.cpp
M  +1    -0    DB/XML/FileWriter.h
M  +1    -2    MainWindow/Window.cpp
M  +0    -1    MainWindow/Window.h
M  +3    -0    Utilities/DescriptionUtil.cpp
A  +126  -0    Viewer/AnnotationHandler.cpp     [License: GPL(v2.0+)]
A  +46   -0    Viewer/AnnotationHandler.h     [License: GPL(v2.0+)]
A  +79   -0    Viewer/CursorVisibilityHandler.cpp     [License: GPL(v2.0+)]
A  +34   -0    Viewer/CursorVisibilityHandler.h     [License: GPL(v2.0+)]
M  +2    -53   Viewer/ImageDisplay.cpp
M  +0    -7    Viewer/ImageDisplay.h
A  +212  -0    Viewer/SelectCategoryAndValue.cpp     [License: GPL(v2.0+)]
A  +38   -0    Viewer/SelectCategoryAndValue.h     [License: GPL(v2.0+)]
A  +175  -0    Viewer/SelectCategoryAndValue.ui
D  +0    -88   Viewer/SpeedDisplay.cpp
A  +78   -0    Viewer/TransientDisplay.cpp     [License: GPL(v2.0+)]
R  +9    -8    Viewer/TransientDisplay.h [from: Viewer/SpeedDisplay.h - 057% similarity]
M  +212  -185  Viewer/ViewerWidget.cpp
M  +28   -20   Viewer/ViewerWidget.h
M  +1    -1    Viewer/documentation.h
A  +-    --    doc/annotation-mode.png
A  +-    --    doc/assign-macro-add-new-value.png
A  +-    --    doc/assign-macro-overview.png
A  +-    --    doc/assign-macro-step1.png
A  +-    --    doc/assign-macro-step2.png
D  +-    --    doc/draw-on-image.png
M  +17   -4    doc/setting-properties.docbook
M  +82   -145  doc/viewer.docbook

https://invent.kde.org/graphics/kphotoalbum/-/commit/3a130ed45089c1b043bea8834a23be0438b4ee17

diff --git a/.reuse/dep5 b/.reuse/dep5
index 8c585b776..d27ce6bd3 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -25,3 +25,7 @@ License: CC0-1.0
 Files: demo/*.jpg demo/*.avi demo/index.xml
 Copyright: 2003-2007 Jesper K. Pedersen
 License: CC-BY-SA-4.0
+
+Files: *.ui
+Copyright: 2023 The KPhotoAlbum Development Team
+License: GPL-2.0-or-later
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60eb8bc2b..bc98b4851 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,8 @@ The change log for older releases (before 5.9.0) can be found in CHANGELOG.old.
 ------------
 
 ### Added
-
+ - Support annotating images from the viewer. Press Ctrl+? in the viewer for details.
+ 
 ### Changed
 
 ### Dependencies
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b67c1a478..411a68726 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,7 @@
 # SPDX-FileCopyrightText: 2007 Gustavo P. Boiko <gustavo.boiko at kdemail.net>
 # SPDX-FileCopyrightText: 2007-2008 Laurent Montel <montel at kde.org>
 # SPDX-FileCopyrightText: 2007-2010 Tuomas Suutari <tuomas at nepnep.net>
-# SPDX-FileCopyrightText: 2007-2014 Jesper K. Pedersen <blackie at kde.org>
+# SPDX-FileCopyrightText: 2007-2023 Jesper K. Pedersen <blackie at kde.org>
 # SPDX-FileCopyrightText: 2008, 2013-2014, 2016 Pino Toscano <pino at kde.org>
 # SPDX-FileCopyrightText: 2008-2009 Henner Zeller <h.zeller at acm.org>
 # SPDX-FileCopyrightText: 2008-2009 Jan Kundrát <jkt at flaska.net>
@@ -36,6 +36,7 @@ math(EXPR MINIMUM_Qt5_VERSION_HEX "${MINIMUM_Qt5_VERSION_MAJOR} << 24 | ${MINIMU
 
 set(CMAKE_CXX_STANDARD 14)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_AUTOUIC ON)
 
 # provide drop-down menu for build-type in cmake-gui:
 set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ";Debug;Release;RelWithDebInfo;MinSizeRel")
@@ -228,8 +229,8 @@ set(libViewer_SRCS
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ImageDisplay.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewHandler.cpp"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/ViewHandler.h"
-    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SpeedDisplay.cpp"
-    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SpeedDisplay.h"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TransientDisplay.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/TransientDisplay.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBox.cpp"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/InfoBox.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/CategoryImageConfig.cpp"
@@ -254,8 +255,19 @@ set(libViewer_SRCS
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/VideoToolBar.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/Slider.cpp"
     "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/Slider.h"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/CursorVisibilityHandler.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/CursorVisibilityHandler.h"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/AnnotationHandler.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/AnnotationHandler.h"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SelectCategoryAndValue.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SelectCategoryAndValue.h"
     )
 
+ki18n_wrap_ui(libViewer_SRCS
+    "${CMAKE_CURRENT_SOURCE_DIR}/Viewer/SelectCategoryAndValue.ui"
+    )
+
+
 set(libCategoryListView_SRCS
     "${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/documentation.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/CategoryListView/DragableTreeWidget.cpp"
@@ -488,6 +500,8 @@ set(libDB_SRCS
     "${CMAKE_CURRENT_SOURCE_DIR}/DB/XML/NumberedBackup.h"
     "${CMAKE_CURRENT_SOURCE_DIR}/DB/XML/XmlReader.cpp"
     "${CMAKE_CURRENT_SOURCE_DIR}/DB/XML/XmlReader.h"
+    "${CMAKE_CURRENT_SOURCE_DIR}/DB/GlobalCategorySortOrder.cpp"
+    "${CMAKE_CURRENT_SOURCE_DIR}/DB/GlobalCategorySortOrder.h"
     )
 
 set(libImportExport_SRCS
@@ -695,7 +709,6 @@ add_subdirectory(scripts)
 
 add_subdirectory(doc)
 
-
 ########### next target ###############
 
 set(kphotoalbum_SRCS
diff --git a/DB/CategoryCollection.cpp b/DB/CategoryCollection.cpp
index 5e4aeb2bf..518d07068 100644
--- a/DB/CategoryCollection.cpp
+++ b/DB/CategoryCollection.cpp
@@ -12,6 +12,8 @@
 
 #include <DB/ImageDB.h>
 
+DB::CategoryCollection::~CategoryCollection() = default;
+
 DB::CategoryPtr DB::CategoryCollection::categoryForName(const QString &name) const
 {
     for (QList<DB::CategoryPtr>::ConstIterator it = m_categories.begin(); it != m_categories.end(); ++it) {
@@ -78,6 +80,11 @@ void DB::CategoryCollection::rename(const QString &oldName, const QString &newNa
     Q_EMIT categoryCollectionChanged();
 }
 
+DB::GlobalCategorySortOrder *DB::CategoryCollection::globalSortOrder()
+{
+    return m_globalSortOrder.get();
+}
+
 void DB::CategoryCollection::initIdMap()
 {
     for (DB::CategoryPtr categoryPtr : qAsConst(m_categories)) {
@@ -95,5 +102,10 @@ void DB::CategoryCollection::slotItemRemoved(const QString &item)
     Q_EMIT itemRemoved(static_cast<Category *>(const_cast<QObject *>(sender())), item);
 }
 
+DB::CategoryCollection::CategoryCollection()
+    : m_globalSortOrder(new GlobalCategorySortOrder())
+{
+}
+
 #include "moc_CategoryCollection.cpp"
 // vi:expandtab:tabstop=4 shiftwidth=4:
diff --git a/DB/CategoryCollection.h b/DB/CategoryCollection.h
index cfdde3cec..5a85d67a7 100644
--- a/DB/CategoryCollection.h
+++ b/DB/CategoryCollection.h
@@ -12,8 +12,9 @@
 
 #include "Category.h"
 #include "CategoryPtr.h"
-
+#include "GlobalCategorySortOrder.h"
 #include <QList>
+#include <memory>
 
 namespace DB
 {
@@ -28,6 +29,8 @@ class CategoryCollection : public QObject
     Q_OBJECT
 
 public:
+    ~CategoryCollection();
+
     enum class IncludeSpecialCategories {
         Yes,
         No
@@ -42,6 +45,7 @@ public:
                      int thumbnailSize, bool show, bool positionable = false);
     void removeCategory(const QString &name);
     void rename(const QString &oldName, const QString &newName);
+    GlobalCategorySortOrder *globalSortOrder();
 
     // FIXME(jzarl): this should be private and FileWriter should be a friend class
     void initIdMap();
@@ -57,8 +61,12 @@ protected Q_SLOTS:
     void slotItemRemoved(const QString &item);
 
 private:
+    friend class ImageDB;
+    CategoryCollection();
+
     QList<DB::CategoryPtr> m_categories;
     QMap<DB::Category::CategoryType, DB::CategoryPtr> m_specialCategories;
+    std::unique_ptr<GlobalCategorySortOrder> m_globalSortOrder;
 };
 
 }
diff --git a/DB/GlobalCategorySortOrder.cpp b/DB/GlobalCategorySortOrder.cpp
new file mode 100644
index 000000000..8e1d9b7d8
--- /dev/null
+++ b/DB/GlobalCategorySortOrder.cpp
@@ -0,0 +1,74 @@
+// SPDX-FileCopyrightText: 2003-2023 The KPhotoAlbum Development Team
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "GlobalCategorySortOrder.h"
+#include "CategoryCollection.h"
+#include "ImageDB.h"
+#include <QDebug>
+#include <algorithm>
+#include <set>
+
+namespace DB
+{
+
+void GlobalCategorySortOrder::pushToFront(const QString &category, const QString &value)
+{
+    const Item item { category, value };
+    m_sortOrder.removeAll(item);
+    m_sortOrder.push_front(item);
+}
+
+QList<GlobalCategorySortOrder::Item> GlobalCategorySortOrder::modifiedSortOrder()
+{
+    return m_sortOrder;
+}
+
+QList<GlobalCategorySortOrder::Item> GlobalCategorySortOrder::completeSortOrder()
+{
+    QList<Item> result;
+    result.append(m_sortOrder);
+
+    // This is the set of all those categories already kept at the front.
+    // We have it as a set to allow fast lookup when going through all categories
+    // and appending them (as those found here should not be appended)
+    QSet<Item> currentSet(m_sortOrder.cbegin(), m_sortOrder.cend());
+
+    // To make it possible to remove entries no longer present in the categories
+    // (either because they were removed or renamed, or gone some other magic way),
+    // we initial copy all categories from m_sortOrder, and then remove them as we see them
+    // while populating from each category
+    QSet<Item> unknownSet = currentSet;
+
+    const auto categories = DB::ImageDB::instance()->categoryCollection()->categories();
+    for (const auto &category : categories) {
+        if (!category->isSpecialCategory() || category->type() == DB::Category::TokensCategory) {
+            auto items = category->items();
+
+            for (const auto &categoryItem : qAsConst(items)) {
+                const Item item { category->name(), categoryItem };
+                unknownSet.remove(item);
+                if (!currentSet.contains(item))
+                    result.append(item);
+            }
+        }
+    }
+
+    for (const auto &item : unknownSet) {
+        result.removeAll(item);
+    }
+
+    return result;
+}
+
+bool operator==(const GlobalCategorySortOrder::Item &x, const GlobalCategorySortOrder::Item &y)
+{
+    return x.category == y.category && x.item == y.item;
+}
+
+size_t qHash(const GlobalCategorySortOrder::Item &item)
+{
+    return qHash(item.category) + qHash(item.item);
+}
+
+} // namespace DB
diff --git a/DB/GlobalCategorySortOrder.h b/DB/GlobalCategorySortOrder.h
new file mode 100644
index 000000000..1f6184787
--- /dev/null
+++ b/DB/GlobalCategorySortOrder.h
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2003-2023 The KPhotoAlbum Development Team
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "CategoryPtr.h"
+#include <QList>
+
+namespace DB
+{
+class FileReader;
+
+/**
+   Just as each category has a most-recently-used sorting,
+   that makes it possible to see the most likely hit in the List boxes in the annotation dialog,
+   we also keep a global most-recent-used list across all categories, so that you can
+   get a sorting like People/Jesper, Places/Las Vegas, People Jim
+   This is what this class encapsulate.
+
+   When an item has been used for tagging, it is pushed to the front of the list using
+   \ref pushToFront.
+
+   To get the global sort order for use for completion, call \ref completeSortOrder.
+
+   On her other hand, to save the sort order, only those ever used should be retrieved,
+   which is possible using \ref modifiedSortOrder
+ */
+class GlobalCategorySortOrder
+{
+public:
+    void pushToFront(const QString &category, const QString &value);
+
+    struct Item {
+        QString category;
+        QString item;
+        friend bool operator==(const Item &x, const Item &y);
+        friend size_t qHash(const Item &item);
+    };
+
+    QList<Item> completeSortOrder();
+    QList<Item> modifiedSortOrder();
+
+private:
+    friend class DB::FileReader;
+    QList<Item> m_sortOrder;
+};
+
+} // namespace DB
diff --git a/DB/ImageDB.cpp b/DB/ImageDB.cpp
index f227f2fc0..c6643a693 100644
--- a/DB/ImageDB.cpp
+++ b/DB/ImageDB.cpp
@@ -814,7 +814,7 @@ const DB::TagInfo *ImageDB::untaggedTag() const
 int ImageDB::fileVersion()
 {
     // File format version, bump it up every time the format for the file changes.
-    return 9;
+    return 10;
 }
 
 ImageInfoPtr ImageDB::createImageInfo(const FileName &fileName, DB::ReaderPtr reader, ImageDB *db, const QMap<QString, QString> *newToOldCategory)
diff --git a/DB/XML/FileReader.cpp b/DB/XML/FileReader.cpp
index 9cabd8688..5b0d80831 100644
--- a/DB/XML/FileReader.cpp
+++ b/DB/XML/FileReader.cpp
@@ -72,6 +72,7 @@ void DB::FileReader::read(const QString &configFile)
     loadBlockList(reader);
     loadMemberGroups(reader);
     // loadSettings(reader);
+    loadGlobalSortOrder(reader);
 
     repairDB();
 
@@ -373,6 +374,22 @@ void DB::FileReader::loadMemberGroups(ReaderPtr reader)
     }
 }
 
+void DB::FileReader::loadGlobalSortOrder(ReaderPtr reader)
+{
+    ElementInfo info = reader->peekNext();
+    auto &list = m_db->categoryCollection()->globalSortOrder()->m_sortOrder;
+
+    if (info.isStartToken && info.tokenName == QStringLiteral("global-sort-order")) {
+        reader->readNextStartOrStopElement(QStringLiteral("item"));
+        while (reader->readNextStartOrStopElement(QStringLiteral("item")).isStartToken) {
+            const QString category = reader->attribute(QStringLiteral("category"));
+            const QString item = reader->attribute(QStringLiteral("item"));
+            list.append({ category, item });
+            reader->readEndElement();
+        }
+    }
+}
+
 /*
 void DB::FileReader::loadSettings(ReaderPtr reader)
 {
diff --git a/DB/XML/FileReader.h b/DB/XML/FileReader.h
index a45280513..04f0b8955 100644
--- a/DB/XML/FileReader.h
+++ b/DB/XML/FileReader.h
@@ -43,6 +43,7 @@ protected:
     void loadBlockList(ReaderPtr reader);
     void loadMemberGroups(ReaderPtr reader);
     // void loadSettings(ReaderPtr reader);
+    void loadGlobalSortOrder(ReaderPtr reader);
 
     DB::ImageInfoPtr load(const DB::FileName &filename, ReaderPtr reader);
     ReaderPtr readConfigFile(const QString &configFile);
diff --git a/DB/XML/FileWriter.cpp b/DB/XML/FileWriter.cpp
index b40c48125..f6879d0d3 100644
--- a/DB/XML/FileWriter.cpp
+++ b/DB/XML/FileWriter.cpp
@@ -97,6 +97,7 @@ void DB::FileWriter::save(const QString &fileName, bool isAutoSave)
         saveBlockList(writer);
         saveMemberGroups(writer);
         // saveSettings(writer);
+        saveGlobalSortOrder(writer);
     }
     writer.writeEndDocument();
     qCDebug(TimingLog) << "DB::FileWriter::save(): Saving took" << timer.elapsed() << "ms";
@@ -264,6 +265,16 @@ void DB::FileWriter::saveMemberGroups(QXmlStreamWriter &writer)
     }
 }
 
+void DB::FileWriter::saveGlobalSortOrder(QXmlStreamWriter &writer)
+{
+    ElementWriter dummy(writer, QStringLiteral("global-sort-order"));
+    for (const auto &item : m_db->categoryCollection()->globalSortOrder()->modifiedSortOrder()) {
+        ElementWriter dummy(writer, QStringLiteral("item"));
+        writer.writeAttribute(QStringLiteral("category"), item.category);
+        writer.writeAttribute(QStringLiteral("item"), item.item);
+    }
+}
+
 /*
 Perhaps, we may need this later ;-)
 
diff --git a/DB/XML/FileWriter.h b/DB/XML/FileWriter.h
index 3db418beb..6fb217763 100644
--- a/DB/XML/FileWriter.h
+++ b/DB/XML/FileWriter.h
@@ -38,6 +38,7 @@ protected:
     void saveImages(QXmlStreamWriter &);
     void saveBlockList(QXmlStreamWriter &);
     void saveMemberGroups(QXmlStreamWriter &);
+    void saveGlobalSortOrder(QXmlStreamWriter&);
     void save(QXmlStreamWriter &writer, const DB::ImageInfoPtr &info);
     void writeCategories(QXmlStreamWriter &, const DB::ImageInfoPtr &info);
     void writeCategoriesCompressed(QXmlStreamWriter &, const DB::ImageInfoPtr &info);
diff --git a/MainWindow/Window.cpp b/MainWindow/Window.cpp
index ab14797aa..81a279dd5 100644
--- a/MainWindow/Window.cpp
+++ b/MainWindow/Window.cpp
@@ -700,8 +700,7 @@ void MainWindow::Window::launchViewer(const DB::FileNameList &inputMediaList, bo
         viewer->raise();
         viewer->activateWindow();
     } else {
-        viewer = new Viewer::ViewerWidget(Viewer::ViewerWidget::ViewerWindow,
-                                          &m_viewerInputMacros);
+        viewer = new Viewer::ViewerWidget(Viewer::ViewerWidget::ViewerWindow);
         viewer->setCopyLinkEngine(m_copyLinkEngine);
     }
 
diff --git a/MainWindow/Window.h b/MainWindow/Window.h
index efb2a69ad..8068eaddb 100644
--- a/MainWindow/Window.h
+++ b/MainWindow/Window.h
@@ -294,7 +294,6 @@ private:
     TokenEditor *m_tokenEditor;
     DateBar::DateBarWidget *m_dateBar;
     QFrame *m_dateBarLine;
-    QMap<Qt::Key, QPair<QString, QString>> m_viewerInputMacros;
     MainWindow::StatusBar *m_statusBar;
     QString m_lastTarget;
 #ifdef HAVE_MARBLE
diff --git a/Utilities/DescriptionUtil.cpp b/Utilities/DescriptionUtil.cpp
index 3358988d0..8c43c30a7 100644
--- a/Utilities/DescriptionUtil.cpp
+++ b/Utilities/DescriptionUtil.cpp
@@ -358,6 +358,9 @@ QString Utilities::formatAge(DB::CategoryPtr category, const QString &item, DB::
 
     const AgeSpec minAge = dateDifference(birthDate, start);
     const AgeSpec maxAge = dateDifference(birthDate, end);
+
+    if (!minAge.isValid() && !maxAge.isValid())
+        return {}; // This is for example a person on an image before their birth
     if (minAge == maxAge)
         return i18n(" (%1)", minAge.format(AgeSpec::I18nContext::Birthday));
     else if (!minAge.isValid())
diff --git a/Viewer/AnnotationHandler.cpp b/Viewer/AnnotationHandler.cpp
new file mode 100644
index 000000000..401513bfc
--- /dev/null
+++ b/Viewer/AnnotationHandler.cpp
@@ -0,0 +1,126 @@
+// SPDX-FileCopyrightText: 2023 Jesper K. Pedersen <jesper.pedersen at kdab.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "AnnotationHandler.h"
+#include "SelectCategoryAndValue.h"
+#include <KConfigGroup>
+#include <KLocalizedString>
+#include <KSharedConfig>
+#include <QDebug>
+#include <QKeyEvent>
+#include <QLocale>
+#include <kpabase/SettingsData.h>
+#include <qnamespace.h>
+
+namespace Viewer
+{
+
+AnnotationHandler::AnnotationHandler(QObject *parent)
+    : QObject(parent)
+{
+    loadSettings();
+}
+
+namespace
+{
+    bool isKeyboardLetter(const QString &txt)
+    {
+        if (txt.isEmpty())
+            return false;
+
+#if 0
+        // See https://www.kdab.com/a-little-hidden-gem-qstringiterator/ for details
+        // Unfortunately this requires a private header, which our CI doesn't like
+        QStringIterator i(txt);
+        char32_t ch = i.next();
+        return QChar::isLetter(ch);
+#else
+        return QLocale().toUpper(txt) != txt;
+#endif
+    }
+};
+
+bool AnnotationHandler::handle(QKeyEvent *event)
+{
+    const auto key = event->text().toLower();
+
+    if (key.isEmpty() || !isKeyboardLetter(key) || (event->modifiers() != Qt::KeyboardModifiers() && event->modifiers() != Qt::KeyboardModifiers(Qt::ShiftModifier)))
+        return false;
+
+    if (!m_assignments.contains(key) || event->modifiers().testFlag(Qt::ShiftModifier)) {
+        const bool assigned = assignKey(key);
+        if (!assigned)
+            return false;
+    }
+    const auto match = m_assignments.value(key);
+
+    Q_EMIT requestToggleCategory(match.category, match.value);
+
+    return true;
+}
+
+bool AnnotationHandler::assignKey(const QString &key)
+{
+    SelectCategoryAndValue dialog(i18nc("@title", "Assign Macro"), i18n("Select item for macro key <b>%1</b>", key), m_assignments);
+    connect(&dialog, &SelectCategoryAndValue::helpRequest, this, &AnnotationHandler::requestHelp);
+
+    auto result = dialog.exec();
+    if (result == QDialog::Rejected)
+        return false;
+
+    m_assignments[key] = { dialog.category(), dialog.value() };
+    saveSettings();
+    return true;
+}
+
+namespace
+{
+    KConfigGroup configGroup()
+    {
+        const auto section = Settings::SettingsData::instance()->groupForDatabase("viewer keybindings");
+        return KSharedConfig::openConfig(QString::fromLatin1("kphotoalbumrc"))->group(section);
+    }
+}
+
+void AnnotationHandler::saveSettings()
+{
+
+    KConfigGroup group = configGroup();
+    for (auto it = m_assignments.cbegin(); it != m_assignments.cend(); ++it) {
+        auto subgroup = group.group(it.key());
+        const auto item = it.value();
+        subgroup.writeEntry("category", item.category);
+        subgroup.writeEntry("value", item.value);
+    }
+    group.sync();
+}
+
+void AnnotationHandler::loadSettings()
+{
+    KConfigGroup group = configGroup();
+    const QStringList keys = group.groupList();
+    for (const QString &key : keys) {
+        auto subgroup = group.group(key);
+        m_assignments[key] = { subgroup.readEntry("category"), subgroup.readEntry("value") };
+    }
+}
+
+bool AnnotationHandler::askForTagAndInsert()
+{
+    SelectCategoryAndValue dialog(i18n("Tag Item"), i18n("Select tag for image"), m_assignments);
+    connect(&dialog, &SelectCategoryAndValue::helpRequest, this, &AnnotationHandler::requestHelp);
+
+    auto result = dialog.exec();
+    if (result == QDialog::Rejected)
+        return false;
+    Q_EMIT requestToggleCategory(dialog.category(), dialog.value());
+    return true;
+}
+
+AnnotationHandler::Assignments AnnotationHandler::assignments() const
+{
+    return m_assignments;
+}
+
+} // namespace Viewer
diff --git a/Viewer/AnnotationHandler.h b/Viewer/AnnotationHandler.h
new file mode 100644
index 000000000..db2618c1a
--- /dev/null
+++ b/Viewer/AnnotationHandler.h
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2023 Jesper K. Pedersen <jesper.pedersen at kdab.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <KLocalizedString>
+#include <QMap>
+#include <QObject>
+
+class QKeyEvent;
+
+namespace Viewer
+{
+
+class AnnotationHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    AnnotationHandler(QObject *parent);
+    bool handle(QKeyEvent *event);
+    bool askForTagAndInsert();
+
+    struct Assignment {
+        QString category;
+        QString value;
+    };
+    using Key = QString;
+    using Assignments = QMap<Key, Assignment>;
+
+    Assignments assignments() const;
+
+Q_SIGNALS:
+    void requestToggleCategory(const QString &category, const QString &value);
+    void requestHelp();
+
+private:
+    bool assignKey(const QString &key);
+    void saveSettings();
+    void loadSettings();
+
+    Assignments m_assignments;
+};
+
+} // namespace Viewer
diff --git a/Viewer/CursorVisibilityHandler.cpp b/Viewer/CursorVisibilityHandler.cpp
new file mode 100644
index 000000000..9633b0ee8
--- /dev/null
+++ b/Viewer/CursorVisibilityHandler.cpp
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2023 The KPhotoAlbum Development Team
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "CursorVisibilityHandler.h"
+#include <QEvent>
+#include <QTimer>
+#include <chrono>
+
+using namespace std::chrono_literals;
+
+CursorVisibilityHandler::CursorVisibilityHandler(QWidget *parentWidget)
+    : QObject(parentWidget)
+    , m_parentWidget(parentWidget)
+    , m_timer(new QTimer(this))
+{
+    m_cursorHidingEnabled.push(true);
+    m_timer->setSingleShot(true);
+    connect(m_timer, &QTimer::timeout, this, &CursorVisibilityHandler::hideCursor);
+    m_parentWidget->installEventFilter(this);
+
+    const auto children = m_parentWidget->findChildren<QWidget *>();
+    for (auto child : children)
+        child->installEventFilter(this);
+}
+
+bool CursorVisibilityHandler::eventFilter(QObject *watched, QEvent *event)
+{
+    switch (event->type()) {
+    case QEvent::MouseButtonPress:
+        // disable cursor hiding till button release
+        disableCursorHiding();
+        break;
+
+    case QEvent::MouseMove:
+        // just reset the timer
+        showCursorTemporarily();
+        break;
+
+    case QEvent::MouseButtonRelease:
+        // enable cursor hiding and reset timer
+        enableCursorHiding();
+        showCursorTemporarily();
+        break;
+
+    default:
+        break;
+    }
+    return QObject::eventFilter(watched, event);
+}
+
+void CursorVisibilityHandler::showCursorTemporarily()
+{
+    if (!m_cursorHidingEnabled.top())
+        return;
+
+    m_parentWidget->unsetCursor();
+    m_timer->start(1500ms);
+}
+
+void CursorVisibilityHandler::disableCursorHiding()
+{
+    m_cursorHidingEnabled.push(false);
+    m_parentWidget->unsetCursor();
+}
+
+void CursorVisibilityHandler::enableCursorHiding()
+{
+    m_cursorHidingEnabled.pop();
+    hideCursor();
+}
+
+void CursorVisibilityHandler::hideCursor()
+{
+    if (!m_cursorHidingEnabled.top())
+        return;
+
+    m_parentWidget->setCursor(Qt::BlankCursor);
+}
diff --git a/Viewer/CursorVisibilityHandler.h b/Viewer/CursorVisibilityHandler.h
new file mode 100644
index 000000000..8cdcd3ba9
--- /dev/null
+++ b/Viewer/CursorVisibilityHandler.h
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2023 The KPhotoAlbum Development Team
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QStack>
+#include <QWidget>
+
+/**
+  This class handles hiding the mouse cursor when it is not needed while viewing images or videos.
+
+  In some situations, e.g. when selecting an area for zooming, or when bringing up the annotation dialog,
+  this handling is temporarily disabled.
+  */
+class CursorVisibilityHandler : public QObject
+{
+    Q_OBJECT
+public:
+    explicit CursorVisibilityHandler(QWidget *parentWidget);
+    void disableCursorHiding();
+    void enableCursorHiding();
+
+protected:
+    bool eventFilter(QObject *watched, QEvent *event) override;
+
+private:
+    void showCursorTemporarily();
+    void hideCursor();
+
+    QWidget *m_parentWidget;
+    QTimer *m_timer;
+    QStack<bool> m_cursorHidingEnabled;
+};
diff --git a/Viewer/ImageDisplay.cpp b/Viewer/ImageDisplay.cpp
index 3bc6a80a6..9449a0ca3 100644
--- a/Viewer/ImageDisplay.cpp
+++ b/Viewer/ImageDisplay.cpp
@@ -64,58 +64,14 @@ Viewer::ImageDisplay::ImageDisplay(QWidget *parent)
     , m_forward(true)
     , m_curIndex(0)
     , m_busy(false)
-    , m_cursorHiding(true)
 {
     m_viewHandler = new ViewHandler(this);
 
     setMouseTracking(true);
-    m_cursorTimer = new QTimer(this);
-    m_cursorTimer->setSingleShot(true);
-    connect(m_cursorTimer, &QTimer::timeout, this, &ImageDisplay::hideCursor);
-    showCursor();
-}
-
-/**
- * If mouse cursor hiding is enabled, hide the cursor right now
- */
-void Viewer::ImageDisplay::hideCursor()
-{
-    if (m_cursorHiding)
-        setCursor(Qt::BlankCursor);
-}
-
-/**
- * If mouse cursor hiding is enabled, show normal cursor and start a timer that will hide it later
- */
-void Viewer::ImageDisplay::showCursor()
-{
-    if (m_cursorHiding) {
-        unsetCursor();
-        m_cursorTimer->start(1500);
-    }
-}
-
-/**
- * Prevent hideCursor() and showCursor() from altering cursor state
- */
-void Viewer::ImageDisplay::disableCursorHiding()
-{
-    m_cursorHiding = false;
-}
-
-/**
- * Enable automatic mouse cursor hiding
- */
-void Viewer::ImageDisplay::enableCursorHiding()
-{
-    m_cursorHiding = true;
 }
 
 void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event)
 {
-    // disable cursor hiding till button release
-    disableCursorHiding();
-
     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
     bool block = m_viewHandler->mousePressEvent(&e, event->pos(), ratio);
@@ -126,9 +82,6 @@ void Viewer::ImageDisplay::mousePressEvent(QMouseEvent *event)
 
 void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event)
 {
-    // just reset the timer
-    showCursor();
-
     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
     bool block = m_viewHandler->mouseMoveEvent(&e, event->pos(), ratio);
@@ -139,10 +92,6 @@ void Viewer::ImageDisplay::mouseMoveEvent(QMouseEvent *event)
 
 void Viewer::ImageDisplay::mouseReleaseEvent(QMouseEvent *event)
 {
-    // enable cursor hiding and reset timer
-    enableCursorHiding();
-    showCursor();
-
     m_cache.remove(m_curIndex);
     QMouseEvent e(event->type(), mapPos(event->pos()), event->button(), event->buttons(), event->modifiers());
     double ratio = sizeRatio(QSize(m_zEnd.x() - m_zStart.x(), m_zEnd.y() - m_zStart.y()), size());
@@ -495,8 +444,8 @@ void Viewer::ImageDisplay::updateZoomCaption()
     }
 
     Q_EMIT setCaptionInfo((ratio > 1.05)
-                            ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString()
-                            : QString());
+                              ? ki18n("[ zoom x%1 ]").subs(ratio, 0, 'f', 1).toString()
+                              : QString());
 }
 
 QImage Viewer::ImageDisplay::currentViewAsThumbnail() const
diff --git a/Viewer/ImageDisplay.h b/Viewer/ImageDisplay.h
index 73fd68eae..91d4ead07 100644
--- a/Viewer/ImageDisplay.h
+++ b/Viewer/ImageDisplay.h
@@ -69,10 +69,6 @@ public Q_SLOTS:
 
 protected Q_SLOTS:
     bool setImageImpl(DB::ImageInfoPtr info, bool forward) override;
-    void hideCursor();
-    void showCursor();
-    void disableCursorHiding();
-    void enableCursorHiding();
 
 Q_SIGNALS:
     void possibleChange();
@@ -127,9 +123,6 @@ private:
     int m_curIndex;
     bool m_busy;
     ViewerWidget *m_viewer;
-
-    QTimer *m_cursorTimer;
-    bool m_cursorHiding;
 };
 }
 
diff --git a/Viewer/SelectCategoryAndValue.cpp b/Viewer/SelectCategoryAndValue.cpp
new file mode 100644
index 000000000..1a1b3d723
--- /dev/null
+++ b/Viewer/SelectCategoryAndValue.cpp
@@ -0,0 +1,212 @@
+// SPDX-FileCopyrightText: 2023 Jesper K. Pedersen <jesper.pedersen at kdab.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "SelectCategoryAndValue.h"
+#include "ui_SelectCategoryAndValue.h"
+#include <DB/CategoryCollection.h>
+#include <DB/ImageDB.h>
+#include <KLocalizedString>
+#include <QCompleter>
+#include <QIdentityProxyModel>
+#include <QPushButton>
+#include <QStandardItem>
+#include <algorithm>
+
+Q_DECLARE_METATYPE(DB::CategoryPtr)
+
+namespace
+{
+/**
+    This proxy model adapts its filtering according to the current search.
+    This is to allow the search to match "Person / Jesper" by simply searching for "Per Je"
+*/
+class AdaptiveFilterProxy : public QIdentityProxyModel
+{
+    Q_OBJECT
+public:
+    using QIdentityProxyModel::QIdentityProxyModel;
+
+    static constexpr int SearchRole = 2000;
+    void setText(const QString &text)
+    {
+        if (text == m_text)
+            return;
+        m_text = text;
+        m_subStrings = text.split(QLatin1String(" "));
+        // This signal must be emitted to invalidate the completor, and force it to filter its rows
+        // again.
+        Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), { SearchRole });
+    }
+
+    QVariant data(const QModelIndex &index, int role) const override
+    {
+        if (role != SearchRole)
+            return QIdentityProxyModel::data(index, role);
+
+        auto matches = [itemText = data(index, Qt::DisplayRole).toString()](const QString &str) {
+            return itemText.contains(str, Qt::CaseInsensitive);
+        };
+
+        if (!m_text.isEmpty() && std::all_of(m_subStrings.cbegin(), m_subStrings.cend(), matches)) {
+            // If the item matches then simply return the search string which will make the
+            // completer include this match.
+            return m_text;
+        } else {
+            // Otherwise return an empty string which will make the completer discard this item.
+            return QString();
+        }
+    }
+
+private:
+    QStringList m_subStrings;
+    QString m_text;
+};
+
+enum Role { Category = Qt::UserRole,
+            Item };
+
+} // namespace
+
+SelectCategoryAndValue::SelectCategoryAndValue(const QString &title, const QString &message, const Viewer::AnnotationHandler::Assignments &assignments, QWidget *parent)
+    : QDialog(parent)
+    , ui(new Ui::SelectCategoryAndValue)
+{
+    ui->setupUi(this);
+    setWindowTitle(title);
+    ui->label->setText(message);
+    setupExistingAssignments(assignments);
+
+    auto model = new QStandardItemModel(this);
+    int row = 0;
+
+    const auto sortOrder = DB::ImageDB::instance()->categoryCollection()->globalSortOrder()->completeSortOrder();
+    for (const auto &item : sortOrder) {
+        auto modelItem = new QStandardItem(QLatin1String("%1 / %2").arg(item.category, item.item));
+        modelItem->setData(item.category, Role::Category);
+        modelItem->setData(item.item, Role::Item);
+        model->insertRow(row++, modelItem);
+    }
+
+    auto searchFilter = new AdaptiveFilterProxy(this);
+    searchFilter->setSourceModel(model);
+
+    auto completer = new QCompleter(this);
+    ui->lineEdit->setCompleter(completer);
+    completer->setModel(searchFilter);
+    completer->setFilterMode(Qt::MatchStartsWith);
+    completer->setCompletionMode(QCompleter::PopupCompletion);
+    completer->setCompletionRole(AdaptiveFilterProxy::SearchRole);
+    completer->setCaseSensitivity(Qt::CaseInsensitive);
+    connect(ui->lineEdit, &QLineEdit::textChanged, searchFilter, &AdaptiveFilterProxy::setText);
+
+    ui->lineEdit->setPlaceholderText(i18n("Type name of category and item"));
+
+    connect(ui->lineEdit, &QLineEdit::returnPressed, this, [this, completer] {
+        auto index = completer->currentIndex();
+        if (index.isValid()) {
+            m_category = index.data(Role::Category).toString();
+            m_item = index.data(Role::Item).toString();
+        }
+        accept();
+    });
+
+    connect(
+        completer, qOverload<const QModelIndex &>(&QCompleter::activated), this,
+        [this](const QModelIndex &index) {
+            m_category = index.data(Role::Category).toString();
+            m_item = index.data(Role::Item).toString();
+            accept();
+        });
+
+    connect(ui->lineEdit, &QLineEdit::textChanged, [completer, this] {
+        ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(completer->currentIndex().isValid());
+    });
+
+    connect(ui->value, &QLineEdit::textChanged, [this](const QString &txt) {
+        ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!txt.isEmpty());
+    });
+
+    ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+
+    ui->addNewTag->setText(i18n("Add New"));
+    connect(ui->addNewTag, &QPushButton::clicked, this, &SelectCategoryAndValue::addNew);
+    ui->categoryLabel->hide();
+    ui->category->hide();
+    ui->titleLabel->hide();
+    ui->value->hide();
+
+    connect(ui->buttonBox, &QDialogButtonBox::helpRequested, this, &SelectCategoryAndValue::helpRequest);
+
+    const auto categories = DB::ImageDB::instance()->categoryCollection()->categories();
+    for (const auto &category : categories) {
+        if (!category->isSpecialCategory())
+            ui->category->addItem(category->name(), QVariant::fromValue(category));
+    }
+}
+
+SelectCategoryAndValue::~SelectCategoryAndValue() = default;
+
+int SelectCategoryAndValue::exec()
+{
+    auto result = QDialog::exec();
+    if (result == QDialog::Rejected)
+        return result;
+
+    if (!ui->category->isHidden()) {
+        m_category = ui->category->currentText();
+        m_item = ui->value->text();
+        ui->category->currentData().value<DB::CategoryPtr>()->addItem(m_item);
+    }
+
+    DB::ImageDB::instance()->categoryCollection()->globalSortOrder()->pushToFront(m_category, m_item);
+    return result;
+}
+
+void SelectCategoryAndValue::addNew()
+{
+    ui->lineEdit->hide();
+    ui->addNewTag->hide();
+    ui->categoryLabel->show();
+    ui->category->show();
+    ui->titleLabel->show();
+    ui->value->show();
+    ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+    ui->value->setText(ui->lineEdit->text());
+    ui->category->setFocus();
+}
+
+void SelectCategoryAndValue::setupExistingAssignments(const Viewer::AnnotationHandler::Assignments &assignments)
+{
+    auto model = new QStandardItemModel(this);
+    model->setHorizontalHeaderLabels(QStringList { i18n("Key"), i18n("Tag") });
+    int row = 0;
+    auto addAssignment = [&](const QString &key, const QString &assignment) {
+        model->setItem(row, 0, new QStandardItem(key));
+        model->setItem(row, 1, new QStandardItem(assignment));
+        ++row;
+    };
+
+    for (auto it = assignments.cbegin(); it != assignments.cend(); ++it) {
+        const Viewer::AnnotationHandler::Assignment assignment = it.value();
+        addAssignment(it.key(), QLatin1String("%1 / %2").arg(assignment.category, assignment.value));
+    }
+
+    ui->knowAssignments->setModel(model);
+    ui->knowAssignments->verticalHeader()->hide();
+    ui->knowAssignments->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+    ui->knowAssignments->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+    ui->knowAssignments->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+}
+
+QString SelectCategoryAndValue::category() const
+{
+    return m_category;
+}
+
+QString SelectCategoryAndValue::value() const
+{
+    return m_item;
+}
+
+#include "SelectCategoryAndValue.moc"
diff --git a/Viewer/SelectCategoryAndValue.h b/Viewer/SelectCategoryAndValue.h
new file mode 100644
index 000000000..09a590710
--- /dev/null
+++ b/Viewer/SelectCategoryAndValue.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2023 Jesper K. Pedersen <jesper.pedersen at kdab.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "AnnotationHandler.h"
+#include <DB/CategoryPtr.h>
+#include <QDialog>
+#include <memory>
+
+namespace Ui
+{
+class SelectCategoryAndValue;
+}
+
+class SelectCategoryAndValue : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit SelectCategoryAndValue(const QString &title, const QString &message, const Viewer::AnnotationHandler::Assignments &assignments, QWidget *parent = nullptr);
+    ~SelectCategoryAndValue();
+    QString category() const;
+    QString value() const;
+    int exec() override;
+
+Q_SIGNALS:
+    void helpRequest();
+
+private:
+    void addNew();
+    void setupExistingAssignments(const Viewer::AnnotationHandler::Assignments &assignments);
+
+    const std::unique_ptr<Ui::SelectCategoryAndValue> ui;
+    QString m_category;
+    QString m_item;
+};
diff --git a/Viewer/SelectCategoryAndValue.ui b/Viewer/SelectCategoryAndValue.ui
new file mode 100644
index 000000000..a2e9513f8
--- /dev/null
+++ b/Viewer/SelectCategoryAndValue.ui
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SelectCategoryAndValue</class>
+ <widget class="QDialog" name="SelectCategoryAndValue">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1024</width>
+    <height>666</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_3">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1">
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>TextLabel</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="lineEdit">
+         <property name="minimumSize">
+          <size>
+           <width>300</width>
+           <height>0</height>
+          </size>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QPushButton" name="addNewTag">
+           <property name="text">
+            <string>Add new Tag</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QFormLayout" name="formLayout">
+         <item row="0" column="0">
+          <widget class="QLabel" name="categoryLabel">
+           <property name="text">
+            <string>Category</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="QComboBox" name="category"/>
+         </item>
+         <item row="1" column="0">
+          <widget class="QLabel" name="titleLabel">
+           <property name="text">
+            <string>Title</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="1">
+          <widget class="QLineEdit" name="value">
+           <property name="minimumSize">
+            <size>
+             <width>250</width>
+             <height>0</height>
+            </size>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="1">
+          <spacer name="verticalSpacer">
+           <property name="orientation">
+            <enum>Qt::Vertical</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>20</width>
+             <height>40</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <widget class="Line" name="line">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="label_2">
+         <property name="text">
+          <string>Existing Assignments</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QTableView" name="knowAssignments"/>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>SelectCategoryAndValue</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>436</x>
+     <y>82</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>52</x>
+     <y>45</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>SelectCategoryAndValue</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>475</x>
+     <y>85</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>595</x>
+     <y>48</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/Viewer/SpeedDisplay.cpp b/Viewer/SpeedDisplay.cpp
deleted file mode 100644
index da99e9096..000000000
--- a/Viewer/SpeedDisplay.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-// SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
-// SPDX-FileCopyrightText: 2022 Johannes Zarl-Zierl <johannes at zarl-zierl.at>
-//
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "SpeedDisplay.h"
-
-#include <KLocalizedString>
-#include <QLabel>
-#include <QLayout>
-#include <QTimeLine>
-#include <QTimer>
-
-Viewer::SpeedDisplay::SpeedDisplay(QWidget *parent)
-    : QLabel(parent)
-{
-    m_timeLine = new QTimeLine(1000, this);
-    connect(m_timeLine, SIGNAL(frameChanged(int)), this, SLOT(setAlphaChannel(int)));
-    m_timeLine->setFrameRange(0, 170);
-    m_timeLine->setDirection(QTimeLine::Backward);
-
-    m_timer = new QTimer(this);
-    m_timer->setSingleShot(true);
-    connect(m_timer, &QTimer::timeout, m_timeLine, &QTimeLine::start);
-
-    setAutoFillBackground(true);
-}
-
-void Viewer::SpeedDisplay::display(int i)
-{
-    // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate
-    // -> dynamically update this text
-    setText(i18nc("OSD for slideshow, num of seconds per image", "<p><center><font size=\"+4\">%1 s</font></center></p>", i / 1000.0));
-    go();
-}
-
-void Viewer::SpeedDisplay::start()
-{
-    // FIXME(jzarl): if the user sets a different shortcut, this is inaccurate
-    // -> dynamically update this text
-    setText(i18nc("OSD for slideshow", "<p><center><font size=\"+4\">Starting Slideshow<br/>Ctrl++ makes the slideshow faster<br/>Ctrl + - makes the slideshow slower</font></center></p>"));
-    go();
-}
-
-void Viewer::SpeedDisplay::go()
-{
-    resize(sizeHint());
-    QWidget *p = static_cast<QWidget *>(parent());
-    move((p->width() - width()) / 2, (p->height() - height()) / 2);
-
-    setAlphaChannel(170, 255);
-    m_timer->start(1000);
-    m_timeLine->stop();
-
-    show();
-    raise();
-}
-
-void Viewer::SpeedDisplay::end()
-{
-    setText(i18nc("OSD for slideshow", "<p><center><font size=\"+4\">Ending Slideshow</font></center></p>"));
-    go();
-}
-
-void Viewer::SpeedDisplay::setAlphaChannel(int backgroundAlpha, int labelAlpha)
-{
-    QPalette p = palette();
-    QColor bgColor = p.window().color();
-    bgColor.setAlpha(backgroundAlpha);
-    p.setColor(QPalette::Window, bgColor);
-    QColor fgColor = p.windowText().color();
-    fgColor.setAlpha(labelAlpha);
-    p.setColor(QPalette::WindowText, fgColor);
-    setPalette(p);
-    // re-enable palette propagation:
-    setAttribute(Qt::WA_SetPalette);
-}
-
-void Viewer::SpeedDisplay::setAlphaChannel(int alpha)
-{
-    setAlphaChannel(alpha, alpha);
-    if (alpha == 0)
-        hide();
-}
-
-// vi:expandtab:tabstop=4 shiftwidth=4:
-
-#include "moc_SpeedDisplay.cpp"
diff --git a/Viewer/TransientDisplay.cpp b/Viewer/TransientDisplay.cpp
new file mode 100644
index 000000000..dd32f4a51
--- /dev/null
+++ b/Viewer/TransientDisplay.cpp
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: 2003-2023 The KPhotoAlbum Development Team
+// SPDX-FileCopyrightText: 2022 Johannes Zarl-Zierl <johannes at zarl-zierl.at>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "TransientDisplay.h"
+
+#include <KLocalizedString>
+#include <QLabel>
+#include <QLayout>
+#include <QTimeLine>
+#include <QTimer>
+
+Viewer::TransientDisplay::TransientDisplay(QWidget *parent)
+    : QLabel(parent)
+{
+    m_timeLine = new QTimeLine(1000, this);
+    connect(m_timeLine, &QTimeLine::frameChanged, this, qOverload<int>(&TransientDisplay::setAlphaChannel));
+    m_timeLine->setFrameRange(0, 170);
+    m_timeLine->setDirection(QTimeLine::Backward);
+
+    m_timer = new QTimer(this);
+    m_timer->setSingleShot(true);
+    connect(m_timer, &QTimer::timeout, [this] {
+        if (m_nextFadeAction == FadeOut)
+            m_timeLine->start();
+        else
+            hide();
+    });
+
+    setAutoFillBackground(true);
+}
+
+void Viewer::TransientDisplay::display(const QString &text, std::chrono::milliseconds duration, FadeAction action)
+{
+    setText(QLatin1String("<p><center><font size=\"+4\">%1</font></center></p>").arg(text));
+    m_nextFadeAction = action;
+    go(duration);
+}
+
+void Viewer::TransientDisplay::go(std::chrono::milliseconds duration)
+{
+    resize(sizeHint());
+    QWidget *p = static_cast<QWidget *>(parent());
+    move((p->width() - width()) / 2, (p->height() - height()) / 2);
+
+    setAlphaChannel(170, 255);
+    m_timer->start(duration);
+    m_timeLine->stop();
+
+    show();
+    raise();
+}
+
+void Viewer::TransientDisplay::setAlphaChannel(int backgroundAlpha, int labelAlpha)
+{
+    QPalette p = palette();
+    QColor bgColor = p.window().color();
+    bgColor.setAlpha(backgroundAlpha);
+    p.setColor(QPalette::Window, bgColor);
+    QColor fgColor = p.windowText().color();
+    fgColor.setAlpha(labelAlpha);
+    p.setColor(QPalette::WindowText, fgColor);
+    setPalette(p);
+    // re-enable palette propagation:
+    setAttribute(Qt::WA_SetPalette);
+}
+
+void Viewer::TransientDisplay::setAlphaChannel(int alpha)
+{
+    setAlphaChannel(alpha, alpha);
+    if (alpha == 0)
+        hide();
+}
+
+// vi:expandtab:tabstop=4 shiftwidth=4:
+
+#include "moc_TransientDisplay.cpp"
diff --git a/Viewer/SpeedDisplay.h b/Viewer/TransientDisplay.h
similarity index 57%
rename from Viewer/SpeedDisplay.h
rename to Viewer/TransientDisplay.h
index 9a73a4769..338ff94e6 100644
--- a/Viewer/SpeedDisplay.h
+++ b/Viewer/TransientDisplay.h
@@ -6,6 +6,7 @@
 #define SPEEDDISPLAY_H
 
 #include <QLabel>
+#include <chrono>
 
 class QTimeLine;
 class QTimer;
@@ -15,24 +16,24 @@ class QHBoxLayout;
 namespace Viewer
 {
 
-class SpeedDisplay : public QLabel
+class TransientDisplay : public QLabel
 {
     Q_OBJECT
 
 public:
-    explicit SpeedDisplay(QWidget *parent);
-    void display(int);
-    void start();
-    void end();
-    void go();
+    explicit TransientDisplay(QWidget *parent);
+    enum FadeAction { FadeOut,
+                      NoFadeOut };
+    void display(const QString &text, std::chrono::milliseconds duration = std::chrono::seconds(1), FadeAction action = FadeOut);
 
-private Q_SLOTS:
+private:
+    void go(std::chrono::milliseconds duration);
     void setAlphaChannel(int alpha);
     void setAlphaChannel(int background, int label);
 
-private:
     QTimer *m_timer;
     QTimeLine *m_timeLine;
+    FadeAction m_nextFadeAction;
 };
 }
 
diff --git a/Viewer/ViewerWidget.cpp b/Viewer/ViewerWidget.cpp
index e3f612ba0..2fd3993f9 100644
--- a/Viewer/ViewerWidget.cpp
+++ b/Viewer/ViewerWidget.cpp
@@ -1,7 +1,7 @@
 // SPDX-FileCopyrightText: 2003 David Faure <faure at kde.org>
 // SPDX-FileCopyrightText: 2003-2005 Stephan Binner <binner at kde.org>
 // SPDX-FileCopyrightText: 2003-2007 Dirk Mueller <mueller at kde.org>
-// SPDX-FileCopyrightText: 2003-2022 Jesper K. Pedersen <jesper.pedersen at kdab.com>
+// SPDX-FileCopyrightText: 2003-2023 Jesper K. Pedersen <jesper.pedersen at kdab.com>
 // SPDX-FileCopyrightText: 2004 Marc Mutz <mutz at kde.org>
 // SPDX-FileCopyrightText: 2006-2010 Tuomas Suutari <tuomas at nepnep.net>
 // SPDX-FileCopyrightText: 2007 Shawn Willden <shawn-kimdaba at willden.org>
@@ -26,6 +26,7 @@
 #include <config-kpa-videobackends.h>
 
 #include "CategoryImageConfig.h"
+#include "CursorVisibilityHandler.h"
 #include "ImageDisplay.h"
 #include "InfoBox.h"
 #include "Logging.h"
@@ -38,14 +39,15 @@
 #include "QtAVDisplay.h"
 #endif
 
-#include "SpeedDisplay.h"
 #include "TaggedArea.h"
 #include "TextDisplay.h"
+#include "TransientDisplay.h"
 
 #if LIBVLC_FOUND
 #include "VLCDisplay.h"
 #endif
 
+#include "AnnotationHandler.h"
 #include "VideoDisplay.h"
 #include "VideoShooter.h"
 #include "VisibleOptionsMenu.h"
@@ -91,9 +93,13 @@
 #include <QWheelEvent>
 #include <qglobal.h>
 
+#include <QDesktopServices>
+#include <QInputDialog>
 #include <QMetaEnum>
 #include <functional>
 
+using namespace std::chrono_literals;
+
 Viewer::ViewerWidget *Viewer::ViewerWidget::s_latest = nullptr;
 
 Viewer::ViewerWidget *Viewer::ViewerWidget::latest()
@@ -102,7 +108,7 @@ Viewer::ViewerWidget *Viewer::ViewerWidget::latest()
 }
 
 // Notice the parent is zero to allow other windows to come on top of it.
-Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap<Qt::Key, QPair<QString, QString>> *macroStore)
+Viewer::ViewerWidget::ViewerWidget(UsageType type)
     : QStackedWidget(nullptr)
     , m_crashSentinel(QString::fromUtf8("videoBackend"))
     , m_current(0)
@@ -112,9 +118,7 @@ Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap<Qt::Key, QPair<QString,
     , m_isRunningSlideShow(false)
     , m_videoPlayerStoppedManually(false)
     , m_type(type)
-    , m_currentCategory(DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name())
-    , m_inputMacros(macroStore)
-    , m_myInputMacros(nullptr)
+    , m_annotationHandler(new AnnotationHandler(this))
 {
     if (type == ViewerWindow) {
         setWindowFlags(Qt::Window);
@@ -122,15 +126,11 @@ Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap<Qt::Key, QPair<QString,
         s_latest = this;
     }
 
-    if (!m_inputMacros) {
-        m_myInputMacros = m_inputMacros = new QMap<Qt::Key, QPair<QString, QString>>;
-    }
-
     m_screenSaverCookie = -1;
-    m_currentInputMode = InACategory;
 
     m_display = m_imageDisplay = new ImageDisplay(this);
     addWidget(m_imageDisplay);
+    m_cursorHandlerForImageDisplay = new CursorVisibilityHandler(m_imageDisplay);
 
     m_textDisplay = new TextDisplay(this);
     addWidget(m_textDisplay);
@@ -153,8 +153,8 @@ Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap<Qt::Key, QPair<QString,
     m_slideShowTimer->setSingleShot(true);
     m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000;
     connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer);
-    m_speedDisplay = new SpeedDisplay(this);
-    m_speedDisplay->hide();
+    m_transientDisplay = new TransientDisplay(this);
+    m_transientDisplay->hide();
 
     setFocusPolicy(Qt::StrongFocus);
 
@@ -164,6 +164,11 @@ Viewer::ViewerWidget::ViewerWidget(UsageType type, QMap<Qt::Key, QPair<QString,
 
     updatePalette();
     connect(Settings::SettingsData::instance(), &Settings::SettingsData::colorSchemeChanged, this, &ViewerWidget::updatePalette);
+
+    connect(m_annotationHandler, &AnnotationHandler::requestToggleCategory,
+            this, &Viewer::ViewerWidget::toggleTag);
+    connect(m_annotationHandler, &AnnotationHandler::requestHelp,
+            this, &Viewer::ViewerWidget::showAnnotationHelp);
 }
 
 void Viewer::ViewerWidget::setupContextMenu()
@@ -171,6 +176,7 @@ void Viewer::ViewerWidget::setupContextMenu()
     m_popup = new QMenu(this);
     m_actions = new KActionCollection(this);
 
+    createAnnotationMenu();
     createSlideShowMenu();
     createZoomMenu();
     createRotateMenu();
@@ -181,11 +187,6 @@ void Viewer::ViewerWidget::setupContextMenu()
     createCategoryImageMenu();
     createFilterMenu();
 
-    QAction *action = m_actions->addAction(QString::fromLatin1("viewer-edit-image-properties"), this, &ViewerWidget::editImage);
-    action->setText(i18nc("@action:inmenu", "Annotate..."));
-    m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1);
-    m_popup->addAction(action);
-
     m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, &ViewerWidget::slotSetStackHead);
     m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack"));
     m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4);
@@ -210,13 +211,13 @@ void Viewer::ViewerWidget::setupContextMenu()
     m_popup->addSeparator();
 
     if (m_type == ViewerWindow) {
-        action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, &ViewerWidget::close);
+        auto action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, &ViewerWidget::close);
         action->setText(i18nc("@action:inmenu", "Close"));
         action->setShortcut(Qt::Key_Escape);
         m_actions->setShortcutsConfigurable(action, false);
+        m_popup->addAction(action);
     }
 
-    m_popup->addAction(action);
     m_actions->readSettings();
 
     const auto actions = m_actions->actions();
@@ -415,12 +416,12 @@ void Viewer::ViewerWidget::createSlideShowMenu()
 
     m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, &ViewerWidget::slotSlideShowFaster);
     m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster"));
-    m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::SpeedDisplay
+    m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::TransientDisplay
     popup->addAction(m_slideShowRunFaster);
 
     m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, &ViewerWidget::slotSlideShowSlower);
     m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower"));
-    m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::SpeedDisplay
+    m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::TransientDisplay
     popup->addAction(m_slideShowRunSlower);
 
     m_popup->addMenu(popup);
@@ -578,6 +579,50 @@ void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action)
         showNextN(0);
 }
 
+void Viewer::ViewerWidget::setTagMode(TagMode tagMode)
+{
+    m_tagMode = tagMode;
+    m_addTagAction->setEnabled(tagMode == TagMode::Annotating);
+    m_copyAction->setEnabled(tagMode == TagMode::Annotating);
+    m_addDescriptionAction->setEnabled(tagMode == TagMode::Annotating);
+
+    const auto tagModeText = [&] {
+        switch (tagMode) {
+        case TagMode::Locked:
+            return i18n("locked");
+        case TagMode::Annotating:
+            return i18n("annotating");
+        case TagMode::Tokenizing:
+            return i18n("tokenizing");
+        }
+        return QString();
+    }();
+
+    m_transientDisplay->display(i18n("Change display mode to %1", tagModeText));
+}
+
+namespace Viewer
+{
+class TemporarilyDisableCursorHandling
+{
+public:
+    TemporarilyDisableCursorHandling(Viewer::ViewerWidget *viewer)
+        : m_viewer(viewer)
+    {
+        viewer->m_cursorHandlerForImageDisplay->disableCursorHiding();
+        viewer->m_cursorHandlerForVideoDisplay->disableCursorHiding();
+    }
+    ~TemporarilyDisableCursorHandling()
+    {
+        m_viewer->m_cursorHandlerForImageDisplay->enableCursorHiding();
+        m_viewer->m_cursorHandlerForVideoDisplay->enableCursorHiding();
+    }
+
+private:
+    Viewer::ViewerWidget *m_viewer;
+};
+}
+
 void Viewer::ViewerWidget::showNext10()
 {
     showNextN(10);
@@ -751,35 +796,19 @@ void Viewer::ViewerWidget::resizeEvent(QResizeEvent *e)
 
 void Viewer::ViewerWidget::updateInfoBox()
 {
-    QString tokensCategory = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name();
-    if (currentInfo() || !m_currentInput.isEmpty() || (!m_currentCategory.isEmpty() && m_currentCategory != tokensCategory)) {
+    if (currentInfo()) {
         QMap<int, QPair<QString, QString>> map;
-        QString text = Utilities::createInfoText(currentInfo(), &map);
-        QString selecttext = QString::fromLatin1("");
-        if (m_currentCategory.isEmpty()) {
-            selecttext = i18nc("Basically 'enter a category name'", "<b>Setting Category: </b>") + m_currentInput;
-            if (m_currentInputList.length() > 0) {
-                selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}");
-            }
-        } else if ((!m_currentInput.isEmpty() && m_currentCategory != tokensCategory)) {
-            selecttext = i18nc("Basically 'enter a tag name'", "<b>Assigning: </b>") + m_currentCategory + QString::fromLatin1("/") + m_currentInput;
-            if (m_currentInputList.length() > 0) {
-                selecttext += QString::fromLatin1("{") + m_currentInputList + QString::fromLatin1("}");
-            }
-        } else if (!m_currentInput.isEmpty() && m_currentCategory == tokensCategory) {
-            m_currentInput = QString::fromLatin1("");
-        }
-        if (!selecttext.isEmpty())
-            text = selecttext + QString::fromLatin1("<br />") + text;
+        const QString text = Utilities::createInfoText(currentInfo(), &map);
+
         if (Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && (m_type != InlineViewer)) {
             m_infoBox->setInfo(text, map);
             m_infoBox->show();
-
         } else
             m_infoBox->hide();
 
         moveInfoBox();
     }
+    m_infoBox->setSize();
 }
 
 Viewer::ViewerWidget::~ViewerWidget()
@@ -788,9 +817,6 @@ Viewer::ViewerWidget::~ViewerWidget()
 
     if (s_latest == this)
         s_latest = nullptr;
-
-    if (m_myInputMacros)
-        delete m_myInputMacros;
 }
 
 void Viewer::ViewerWidget::toggleFullScreen()
@@ -807,13 +833,17 @@ void Viewer::ViewerWidget::slotStartStopSlideShow()
         m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow"));
         m_slideShowTimer->stop();
         if (m_list.count() != 1)
-            m_speedDisplay->end();
+            m_transientDisplay->display(i18nc("OSD for slideshow", "Ending Slideshow"));
         inhibitScreenSaver(false);
     } else {
         m_startStopSlideShow->setText(i18nc("@action:inmenu", "Stop Slideshow"));
         if (currentInfo()->mediaType() != DB::Video)
             m_slideShowTimer->start(m_slideShowPause);
-        m_speedDisplay->start();
+        const auto faster = m_actions->action(QString::fromLatin1("viewer-run-faster"))->shortcut().toString();
+        const auto slower = m_actions->action(QString::fromLatin1("viewer-run-slower"))->shortcut().toString();
+        m_transientDisplay->display(i18nc("OSD for slideshow", "Starting Slideshow<br/>%1 makes the slideshow faster<br/>%2 makes the slideshow slower",
+                                          faster, slower),
+                                    1500ms);
         inhibitScreenSaver(true);
     }
 }
@@ -861,7 +891,7 @@ void Viewer::ViewerWidget::changeSlideShowInterval(int delta)
 
     m_slideShowPause += delta;
     m_slideShowPause = qMax(m_slideShowPause, 500);
-    m_speedDisplay->display(m_slideShowPause);
+    m_transientDisplay->display(i18nc("OSD for slideshow, num of seconds per image", "%1 s", m_slideShowPause / 1000.0));
     if (m_slideShowTimer->isActive())
         m_slideShowTimer->start(m_slideShowPause);
 }
@@ -1016,148 +1046,30 @@ KActionCollection *Viewer::ViewerWidget::actions()
     return m_actions;
 }
 
-int Viewer::ViewerWidget::find_tag_in_list(const QStringList &list,
-                                           QString &namefound)
-{
-    int found = 0;
-    m_currentInputList = QString::fromLatin1("");
-    for (QStringList::ConstIterator listIter = list.constBegin();
-         listIter != list.constEnd(); ++listIter) {
-        if (listIter->startsWith(m_currentInput, Qt::CaseInsensitive)) {
-            found++;
-            if (m_currentInputList.length() > 0)
-                m_currentInputList = m_currentInputList + QString::fromLatin1(",");
-            m_currentInputList = m_currentInputList + listIter->right(listIter->length() - m_currentInput.length());
-            if (found > 1 && m_currentInputList.length() > 20) {
-                // already found more than we want to display
-                // bail here for now
-                // XXX: non-ideal?  display more?  certainly config 20
-                return found;
-            } else {
-                namefound = *listIter;
-            }
-        }
-    }
-    return found;
-}
-
 void Viewer::ViewerWidget::keyPressEvent(QKeyEvent *event)
 {
-
-    if (event->key() == Qt::Key_Backspace) {
-        // remove stuff from the current input string
-        m_currentInput.remove(m_currentInput.length() - 1, 1);
-        updateInfoBox();
-        MainWindow::DirtyIndicator::markDirty();
-        m_currentInputList = QString::fromLatin1("");
-        //     } else if (event->modifier & (Qt::AltModifier | Qt::MetaModifier) &&
-        //                event->key() == Qt::Key_Enter) {
-        return; // we've handled it
-    } else if (event->key() == Qt::Key_Comma) {
-        // force set the "new" token
-        if (!m_currentCategory.isEmpty()) {
-            if (m_currentInput.left(1) == QString::fromLatin1("\"") ||
-                // allow a starting ' or " to signal a brand new category
-                // this bypasses the auto-selection of matching characters
-                m_currentInput.left(1) == QString::fromLatin1("\'")) {
-                m_currentInput = m_currentInput.right(m_currentInput.length() - 1);
-            }
-            if (m_currentInput.isEmpty())
-                return;
-            currentInfo()->addCategoryInfo(m_currentCategory, m_currentInput);
-            DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory);
-            category->addItem(m_currentInput);
-        }
-        m_currentInput = QString::fromLatin1("");
-        updateInfoBox();
-        MainWindow::DirtyIndicator::markDirty();
-        return; // we've handled it
-    } else if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) {
-        bool ok;
-        short rating = event->text().left(1).toShort(&ok, 10);
-        if (ok) {
-            currentInfo()->setRating(rating * 2);
-            updateInfoBox();
-            MainWindow::DirtyIndicator::markDirty();
-        }
-    } else if (event->modifiers() == 0 || event->modifiers() == Qt::ShiftModifier) {
-        // search the category for matches
-        QString namefound;
-        QString incomingKey = event->text().left(1);
-
-        // start searching for a new category name
-        if (incomingKey == QString::fromLatin1("/")) {
-            if (m_currentInput.isEmpty() && m_currentCategory.isEmpty()) {
-                if (m_currentInputMode == InACategory) {
-                    m_currentInputMode = AlwaysStartWithCategory;
-                } else {
-                    m_currentInputMode = InACategory;
-                }
-            } else {
-                // reset the category to search through
-                m_currentInput = QString::fromLatin1("");
-                m_currentCategory = QString::fromLatin1("");
-            }
-
-            // use an assigned key or map to a given key for future reference
-        } else if (m_currentInput.isEmpty() &&
-                   // can map to function keys
-                   event->key() >= Qt::Key_F1 && event->key() <= Qt::Key_F35) {
-
-            // we have a request to assign a macro key or use one
-            Qt::Key key = (Qt::Key)event->key();
-            if (m_inputMacros->contains(key)) {
-                // Use the requested toggle
-                if (event->modifiers() == Qt::ShiftModifier) {
-                    if (currentInfo()->hasCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second)) {
-                        currentInfo()->removeCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second);
-                    }
-                } else {
-                    currentInfo()->addCategoryInfo((*m_inputMacros)[key].first, (*m_inputMacros)[key].second);
-                }
-            } else {
-                (*m_inputMacros)[key] = qMakePair(m_lastCategory, m_lastFound);
-            }
-            updateInfoBox();
-            MainWindow::DirtyIndicator::markDirty();
-            // handled it
+    bool dirty = false;
+    // Rating of the image
+    if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) {
+        const auto rating = event->key() - Qt::Key_0;
+        currentInfo()->setRating(rating * 2);
+        dirty = true;
+    } else if (m_tagMode == TagMode::Locked) {
+        return;
+    } else if (m_tagMode == TagMode::Tokenizing) {
+        if (event->key() < Qt::Key_A || event->key() > Qt::Key_Z)
             return;
-        } else if (m_currentCategory.isEmpty()) {
-            // still searching for a category to lock to
-            m_currentInput += incomingKey;
-            QStringList categorynames = DB::ImageDB::instance()->categoryCollection()->categoryNames();
-            if (find_tag_in_list(categorynames, namefound) == 1) {
-                // yay, we have exactly one!
-                m_currentCategory = namefound;
-                m_currentInput = QString::fromLatin1("");
-                m_currentInputList = QString::fromLatin1("");
-            }
-        } else {
-            m_currentInput += incomingKey;
-
-            DB::CategoryPtr category = DB::ImageDB::instance()->categoryCollection()->categoryForName(m_currentCategory);
-            QStringList items = category->items();
-            if (find_tag_in_list(items, namefound) == 1) {
-                // yay, we have exactly one!
-                if (currentInfo()->hasCategoryInfo(category->name(), namefound))
-                    currentInfo()->removeCategoryInfo(category->name(), namefound);
-                else
-                    currentInfo()->addCategoryInfo(category->name(), namefound);
-
-                m_lastFound = namefound;
-                m_lastCategory = m_currentCategory;
-                m_currentInput = QString::fromLatin1("");
-                m_currentInputList = QString::fromLatin1("");
-                if (m_currentInputMode == AlwaysStartWithCategory)
-                    m_currentCategory = QString::fromLatin1("");
-            }
-        }
 
-        updateInfoBox();
-        MainWindow::DirtyIndicator::markDirty();
+        auto category = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name();
+        toggleTag(category, event->text());
+    } else {
+        TemporarilyDisableCursorHandling dummy(this);
+        dirty = m_annotationHandler->handle(event);
     }
-    QWidget::keyPressEvent(event);
-    return;
+
+    updateInfoBox();
+    if (dirty)
+        MainWindow::DirtyIndicator::markDirty();
 }
 
 void Viewer::ViewerWidget::videoStopped()
@@ -1209,6 +1121,31 @@ void Viewer::ViewerWidget::makeThumbnailImage()
     VideoShooter::go(currentInfo(), this);
 }
 
+void Viewer::ViewerWidget::addTag()
+{
+    TemporarilyDisableCursorHandling dummy(this);
+    const bool dirty = m_annotationHandler->askForTagAndInsert();
+    if (dirty)
+        MainWindow::DirtyIndicator::markDirty();
+}
+
+void Viewer::ViewerWidget::editDescription()
+{
+    TemporarilyDisableCursorHandling dummy(this);
+    const auto description = currentInfo()->description();
+    bool ok;
+    auto newDescription = QInputDialog::getMultiLineText(this, i18nc("@title", "Edit Image Description"), i18nc("@label:textbox", "Image Description"), description, &ok);
+    if (ok && description != newDescription) {
+        currentInfo()->setDescription(newDescription);
+        MainWindow::DirtyIndicator::markDirty();
+    }
+}
+
+void Viewer::ViewerWidget::showAnnotationHelp()
+{
+    QDesktopServices::openUrl(QUrl(QLatin1String("help:/kphotoalbum/chp-viewer.html#annotating-from-the-viewer")));
+}
+
 void Viewer::ViewerWidget::createVideoMenu()
 {
     QMenu *menu = new QMenu(m_popup);
@@ -1434,6 +1371,59 @@ void Viewer::ViewerWidget::createVideoViewer()
 
     addWidget(m_videoDisplay);
     connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped);
+    m_cursorHandlerForVideoDisplay = new CursorVisibilityHandler(m_videoDisplay);
+}
+
+void Viewer::ViewerWidget::createAnnotationMenu()
+{
+    auto menu = new QMenu(i18n("Annotate"));
+
+    auto addAction = [&](const char *name, const QString &title, auto slot, auto shortCut) {
+        QAction *action = m_actions->addAction(QString::fromLatin1(name), this, slot);
+        action->setText(title);
+        m_actions->setDefaultShortcut(action, shortCut);
+        menu->addAction(action);
+        return action;
+    };
+
+    auto toggleGroup = new QActionGroup(this);
+    auto addTagAction = [&](const char *name, const QString &title, TagMode mode, auto shortCut) {
+        auto action = addAction(
+            name, title, [this, mode] { setTagMode(mode); }, shortCut);
+        action->setCheckable(true);
+        toggleGroup->addAction(action);
+        return action;
+    };
+
+    addAction("viewer-show-keybindings", i18nc("@action:inmenu", "Help"), &ViewerWidget::showAnnotationHelp, Qt::CTRL + Qt::Key_Question);
+
+    addAction("viewer-edit-image-properties", i18nc("@action:inmenu", "Annotation Dialog"), &ViewerWidget::editImage, Qt::CTRL + Qt::Key_1);
+    m_addTagAction = addAction("viewer-add-tag", i18nc("@action:inmenu", "Add tag"), &ViewerWidget::addTag, i18nc("short cut for add tag", "CTRL+a"));
+    m_addTagAction->setEnabled(false);
+
+    m_copyAction = addAction("viewer-copy-tag-from-previous-image", i18nc("@action:inmenu", "Copy Data from Previous Image"), &ViewerWidget::copyTagsFromPreviousImage,
+                             i18nc("Shortcut for copy annotations from previous image", "CTRL+c"));
+    m_copyAction->setEnabled(false);
+
+    m_addDescriptionAction = addAction("viewer-edit-description", i18nc("@action:inmenu", "Edit Description"), &ViewerWidget::editDescription,
+                                       i18nc("Shortcut for add description to image", "CTRL+d"));
+    m_addDescriptionAction->setEnabled(false);
+
+    menu->addSection(i18n("Annotation Mode"));
+    auto action = addTagAction(
+        "viewer-tagmode-locked", i18nc("@action:inmenu", "Locked"), TagMode::Locked,
+        i18nc("Shortcut for turning of annotations in the viewer", "CTRL+l"));
+    action->setChecked(true);
+
+    addTagAction(
+        "viewer-tagmode-annotating", i18nc("@action:inmenu", "Assign Tags"), TagMode::Annotating,
+        i18nc("Shortcut for turning annotations mode to annotating", "F2"));
+
+    addTagAction(
+        "viewer-tagmode-tokenizing", i18nc("@action:inmenu", "Assign Tokens"), TagMode::Tokenizing,
+        i18nc("Shortcut for turning annotations mode to tokenizing", "CTRL+t"));
+
+    m_popup->addMenu(menu);
 }
 
 void Viewer::ViewerWidget::stopPlayback()
@@ -1561,6 +1551,43 @@ void Viewer::ViewerWidget::triggerCopyLinkAction(MainWindow::CopyLinkEngine::Act
     m_copyLinkEngine->selectTarget(this, selectedFiles, action);
 }
 
+void Viewer::ViewerWidget::toggleTag(const QString &category, QString value)
+{
+    const bool set = !currentInfo()->hasCategoryInfo(category, value);
+    if (set)
+        currentInfo()->addCategoryInfo(category, value);
+    else
+        currentInfo()->removeCategoryInfo(category, value);
+
+    // Assume we've now annotated this image - this is to avoid removing the untagged item all the time.
+    currentInfo()->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag());
+    updateInfoBox();
+
+    if (category == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name())
+        value = i18n("Token %1", value.toUpper());
+    m_transientDisplay->display(set ? value : QLatin1String("<s>%1</s>").arg(value), 500ms, TransientDisplay::NoFadeOut);
+}
+
+void Viewer::ViewerWidget::copyTagsFromPreviousImage()
+{
+    // Search for the previous image - that is the first one not deleted
+    int index = m_current - 1;
+    while (index >= 0) {
+        const auto fileName = m_list.at(index);
+        if (!m_removed.contains(fileName))
+            break;
+        --index;
+    }
+    if (index == -1)
+        return; // Nothing found
+
+    const auto prevImage = DB::ImageDB::instance()->info(m_list[index]);
+    currentInfo()->merge(*prevImage);
+
+    updateInfoBox();
+    MainWindow::DirtyIndicator::markDirty();
+}
+
 // vi:expandtab:tabstop=4 shiftwidth=4:
 
 #include "moc_ViewerWidget.cpp"
diff --git a/Viewer/ViewerWidget.h b/Viewer/ViewerWidget.h
index a2f2e1568..886514cfc 100644
--- a/Viewer/ViewerWidget.h
+++ b/Viewer/ViewerWidget.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2003-2022 The KPhotoAlbum Development Team
+// SPDX-FileCopyrightText: 2003-2023 The KPhotoAlbum Development Team
 //
 // SPDX-License-Identifier: GPL-2.0-or-later
 
@@ -25,6 +25,8 @@ class QMenu;
 class QResizeEvent;
 class QStackedWidget;
 class QWheelEvent;
+class CursorVisibilityHandler;
+class QLabel;
 
 namespace DB
 {
@@ -42,10 +44,12 @@ class InfoDialog;
 }
 namespace Viewer
 {
+
+class AnnotationHandler;
 class AbstractDisplay;
 class ImageDisplay;
 class InfoBox;
-class SpeedDisplay;
+class TransientDisplay;
 class TextDisplay;
 class VideoDisplay;
 class VideoShooter;
@@ -57,8 +61,7 @@ public:
     enum UsageType { InlineViewer,
                      ViewerWindow };
 
-    ViewerWidget(UsageType type = ViewerWindow,
-                 QMap<Qt::Key, QPair<QString, QString>> *macroStore = nullptr);
+    ViewerWidget(UsageType type = ViewerWindow);
     ~ViewerWidget() override;
     static ViewerWidget *latest();
     void load(const DB::FileNameList &list, int index = 0);
@@ -121,6 +124,7 @@ protected:
     void createFilterMenu();
     void changeSlideShowInterval(int delta);
     void createVideoViewer();
+    void createAnnotationMenu();
     void inhibitScreenSaver(bool inhibit);
     DB::ImageInfoPtr currentInfo() const;
     friend class InfoBox;
@@ -130,12 +134,16 @@ protected:
 private:
     void showNextN(int);
     void showPrevN(int);
-    int find_tag_in_list(const QStringList &list, QString &namefound);
     void invalidateThumbnail() const;
     enum RemoveAction { RemoveImageFromDatabase,
                         OnlyRemoveFromViewer };
     void removeOrDeleteCurrent(RemoveAction);
 
+    enum class TagMode { Locked,
+                         Annotating,
+                         Tokenizing };
+    void setTagMode(TagMode tagMode);
+
 protected Q_SLOTS:
     void showNext();
     void showNext10();
@@ -174,6 +182,9 @@ protected Q_SLOTS:
     void zoomFull();
     void zoomPixelForPixel();
     void makeThumbnailImage();
+    void addTag();
+    void editDescription();
+    void showAnnotationHelp();
 
     /** Set the current window title (filename) and add the given detail */
     void setCaptionWithDetail(const QString &detail);
@@ -185,10 +196,13 @@ protected Q_SLOTS:
     void slotRemoveDeletedImages(const DB::FileNameList &imageList);
 
     void triggerCopyLinkAction(MainWindow::CopyLinkEngine::Action action);
+    void toggleTag(const QString &category, QString value);
+    void copyTagsFromPreviousImage();
 
 private:
     static ViewerWidget *s_latest;
     friend class VideoShooter;
+    friend class TemporarilyDisableCursorHandling;
 
     QList<QAction *> m_forwardActions;
     QList<QAction *> m_backwardActions;
@@ -233,12 +247,11 @@ private:
     QAction *m_linkToAction;
 
     InfoBox *m_infoBox;
-    QImage m_currentImage;
 
     bool m_showingFullScreen;
 
     int m_slideShowPause;
-    SpeedDisplay *m_speedDisplay;
+    TransientDisplay *m_transientDisplay;
     KActionCollection *m_actions;
     bool m_forward;
     QTimer *m_slideShowTimer;
@@ -251,20 +264,15 @@ private:
     bool m_videoPlayerStoppedManually;
     UsageType m_type;
 
-    enum InputMode { InACategory,
-                     AlwaysStartWithCategory };
-
-    InputMode m_currentInputMode;
-    QString m_currentInput;
-    QString m_currentCategory;
-    QString m_currentInputList;
-
-    QString m_lastFound;
-    QString m_lastCategory;
-    QMap<Qt::Key, QPair<QString, QString>> *m_inputMacros;
-    QMap<Qt::Key, QPair<QString, QString>> *m_myInputMacros;
-
     MainWindow::CopyLinkEngine *m_copyLinkEngine;
+    CursorVisibilityHandler *m_cursorHandlerForImageDisplay;
+    CursorVisibilityHandler *m_cursorHandlerForVideoDisplay;
+    AnnotationHandler *m_annotationHandler;
+
+    TagMode m_tagMode = TagMode::Locked;
+    QAction *m_addTagAction;
+    QAction *m_copyAction;
+    QAction *m_addDescriptionAction;
 };
 
 }
diff --git a/Viewer/documentation.h b/Viewer/documentation.h
index e31c73355..2db6b10bd 100644
--- a/Viewer/documentation.h
+++ b/Viewer/documentation.h
@@ -15,7 +15,7 @@
  * <li>\ref AbstractDisplay, \ref ImageDisplay, \ref VideoDisplay and \ref TextDisplay - Widgets hierarchy which takes care of the actual displaying of content.
  * <li> \ref ViewHandler - Handler which interprets mouse gestures.
  * <li> \ref InfoBox - Widget implementing the informatiom box
- * <li> \ref SpeedDisplay - Widget implementing the toplevel display used when adjusting slideshow speed.
+ * <li> \ref TransientDisplay - Widget implementing the toplevel display used for transient messages in the viewer.
  * <li> \ref VideoShooter - Utility class helping with taking a screenshot of a video frame
  * </ul>
  */
diff --git a/doc/annotation-mode.png b/doc/annotation-mode.png
new file mode 100644
index 000000000..8e1a64741
Binary files /dev/null and b/doc/annotation-mode.png differ
diff --git a/doc/assign-macro-add-new-value.png b/doc/assign-macro-add-new-value.png
new file mode 100644
index 000000000..5aaf9219f
Binary files /dev/null and b/doc/assign-macro-add-new-value.png differ
diff --git a/doc/assign-macro-overview.png b/doc/assign-macro-overview.png
new file mode 100644
index 000000000..0b0d26c15
Binary files /dev/null and b/doc/assign-macro-overview.png differ
diff --git a/doc/assign-macro-step1.png b/doc/assign-macro-step1.png
new file mode 100644
index 000000000..1c5af1010
Binary files /dev/null and b/doc/assign-macro-step1.png differ
diff --git a/doc/assign-macro-step2.png b/doc/assign-macro-step2.png
new file mode 100644
index 000000000..8b6edb714
Binary files /dev/null and b/doc/assign-macro-step2.png differ
diff --git a/doc/draw-on-image.png b/doc/draw-on-image.png
deleted file mode 100644
index 8a2d9a06e..000000000
Binary files a/doc/draw-on-image.png and /dev/null differ
diff --git a/doc/setting-properties.docbook b/doc/setting-properties.docbook
index 168779a0f..ebe9c8483 100644
--- a/doc/setting-properties.docbook
+++ b/doc/setting-properties.docbook
@@ -104,9 +104,7 @@ it out. Thus typing <literal>Do</literal> might be enough to find
 <literal>Donna</literal>. Once you have found the item you search for,
 simply press enter to select this item in the listbox. The item will now be
 moved to the top of the view, so next time you need the given item it is
-even easier to find. There is also a "seamless" way of annotating from Image
-Viewer that is described later on in <xref
-linkend="category-selectors-in-viewer" />.</para>
+even easier to find.</para>
 
       <para>The idea behind moving items to the top when they have been
 selected is, that when going to, say a family party, you will get perhaps
@@ -222,7 +220,22 @@ window layout.</para>
 
   </sect1>
 
-  </chapter>
+<sect1>
+    <title>Annotating while viewing images</title>
+    <para>For a few decades we tagged our images as described above, but in the summer of 2023, it started being possible to tag images while viewing them.
+    This facility doesn't support all the features described above, but it offers a much faster workflow to tagging images.</para>
+    <para>Missing features when annotating images from the viewer, which you need to come to this dialog for are:
+    <itemizedlist>
+    <listitem><para>Annotating multiple images at a time</para></listitem>
+    <listitem><para>Tagging individual parts of an image (this face is ...)</para></listitem>
+    <listitem><para>Setting up tag groups (ie. specifying that Las Vegas is in USA, so you will see images tagged Las Vegas when watching images from USA)</para></listitem>
+    <listitem><para>Setting image dates</para></listitem>
+    </itemizedlist>
+
+    <link linkend="annotating-from-the-viewer">See the seciton on annotating from the viewer</link>
+    </para>
+</sect1>
+</chapter>
 
 
 <!-- Keep this comment at the end of the file
diff --git a/doc/viewer.docbook b/doc/viewer.docbook
index f0042dc06..696cd9e16 100644
--- a/doc/viewer.docbook
+++ b/doc/viewer.docbook
@@ -126,21 +126,48 @@ image, simply by choosing <guimenuitem>Annotate</guimenuitem> in the context men
 
 
 
+<sect1 id="annotating-from-the-viewer">
+<title>Annotating images from the viewer</title>
+<para>Traditionally the main way for tagging images has been though <link linkend="chp-typingIn">the annotation dialog</link>.
+Over the years a few different attempts have been made at integrating tagging into the viewer, first attempt was by assigning tokens in the viewer, a later
+attempt was by making it possible to tag directly by typing part of a category item. In addition category items could be assigned to function keys on the keyboard.
+The code for all of this have now been modernized to make the work flow much more intuitive and discover-able.</para>
+
+<para>There are three modes to annotating images in the viewer:
+<itemizedlist>
+    <listitem><para><emphasis>Locked</emphasis> - This is the default mode when starting the viewer - in here no annotating or tokenizing will happen. This ensure you do not accidentally annotates images when viewing them.</para></listitem>
+    <listitem><para><emphasis>Assign Tokens</emphasis> - This allows you to set tokens on images, by simply pressing a key from A-Z on the keyboard.</para></listitem>
+    <listitem><para><emphasis>Assign Tags</emphasis> - This allows you to assign macros to a keyboard key. As an example, <emphasis>L</emphasis> could mean set the item <emphasis>Las Vegas</emphasis> in the <emphasis>Location</emphasis> category. In addition to that, it also allows you to set any tag on an image.</para></listitem>
+</itemizedlist>
+
+The modes are selected from the context menu as can be seen in <xref linkend="fig-ctxmenu-for-mode-selection"/>.
+
+The two modes are described in details below.
+</para>
+
+  <figure id="fig-ctxmenu-for-mode-selection">
+    <title>Selecting annotation mode from the viewer</title>
+    <mediaobject>
+      <imageobject>
+        <imagedata fileref="annotation-mode.png"
+          format="PNG"/>
+      </imageobject>
+    </mediaobject>
+  </figure>
+
+</sect1>
+
+
+
+
 <sect1 id="tokens">
 <title>Setting Tokens from the Viewer</title>
-<para>When viewing the images you may find that a given image contains a person
-whose name you forgot to set on the images. At this time you may cancel
-your viewing, and rush to the image configuration dialog to specify the
-person. However, you may prefer to just tag the image and continue on
-viewing images.</para>
-
-<para>An alternative situation is if you want to sent a number of images
+<para>Imagine you want to sent a number of images
 to a printer to get them developed on paper. To see which you want, you
-start the viewer on the images, and tag them as good or bad while by
-inspecting see each one.</para>
+start the viewer on the images, and tag them as <emphasis>good</emphasis>, <emphasis>bad</emphasis>, or <emphasis>maybe</emphasis>, while inspecting each one.</para>
 
-<para>For the above two examples the viewer offers you to set tokens on the
-images when viewing them. Tokens are named from A to Z, and you set a token
+<para>You may of course create a few new items (say <emphasis>Good</emphasis>, <emphasis>Bad</emphasis>, <emphasis>Maybe</emphasis>) for an existing category, say the category <emphasis>Album</emphasis>.
+However, given that you likely do not need these tags once you've send the images to the printer, there is an easier way, namely by simply setting tokens on the images. Tokens are named from A to Z, and you set a token
 simply by pressing its letter. In <xref linkend="fig-images-with-tokens"/>
 you may see an image where the tokens A, B and C are set.</para>
 
@@ -154,13 +181,12 @@ you may see an image where the tokens A, B and C are set.</para>
     </mediaobject>
   </figure>
 
+  <para>For this to work, you need to be in the <emphasis>Assign Tokens</emphasis> mode. See <xref linkend="fig-ctxmenu-for-mode-selection"/></para>
+
 <para>Once you've set tokens on your images, they will be available for
 regular browsing in the browser, as can be seen in <xref
 linkend="fig-tokens-in-browser"/>. So when you've marked images that
-needs to be edited, printed, or whatever, simply browse to the images, and
-annotate all images at a time (as described in <xref
-linkend="sect-specifying-properties-for-all-images-simultaneously"/>), use
-a plugin to copy the selected images to a CD, or whatever you need to do.</para>
+needs to be edited, printed, or whatever, simply browse to the images, and process the set from there, e.g. by using <emphasis>copy images to...</emphasis> from the context menu.</para>
 
   <figure id="fig-tokens-in-browser">
     <title>Tokens seen in the Browser</title>
@@ -192,143 +218,54 @@ image is selected, this can be seen in
 
 </sect1>
 
-<sect1 id="category-selectors-in-viewer">
-    <title>Setting Categories from the Viewer</title>
-    <para>Category can be selected on viewer by starting to type the category
-    name. Selection is made immediately when there is only one matching
-    category left.</para>
-
-    <para>To switch to category selection mode simply type slash (/). Possible
-    options are shown after Assigning: text in curly brackets and it is
-    immediately revised once more letters are typed. When a category is
-    selected you can keep on typing to select the item value. Following image
-    shows this in action and below it is explanation how this all works.
-    </para>
-
-
-<figure id="category-selectors">
-<title>Selecting Categories in Viewer</title>
-<mediaobject>
-<imageobject>
-<imagedata fileref="category-selectors.png" format="PNG"/>
-</imageobject>
-</mediaobject>
+<sect1>
+    <title>Tagging images from the viewer</title>
+<para>Tagging images can be tiresome, even in &kphotoalbum;, at least when you've fallen behind and have thousands or tens of thousands of images in need of proper tagging.
+    Fortunately, it is now possible to add tags with the press of a single key when viewing the images. The realization behind this way of working with images is that most images are with the same few people in the same few places, plus a few "guest" appearances from time to time - say the location of your vacation, or some friends who were visiting you for a week.</para>
+
+<para>To tag your images while viewing them, you need to enter the <emphasis>Assign Tags mode</emphasis> - see <xref linkend="fig-ctxmenu-for-mode-selection"/>.
+    With this enable, simply press a letter key on your keyboard to either assign or use the assigned tag.</para>
+
+<para>In <xref linkend="fig-assign-macro-step1"/> below, I've just pressed <keycap>s</keycap> while watching images and being in the <emphasis>Assign Tags mode</emphasis>. This brought up the dialog where I can specify what tag to assign to that key.</para>
+
+<figure id="fig-assign-macro-step1">
+  <title>Step 1 - assigning a macro to the key <emphasis>s</emphasis></title>
+  <mediaobject>
+    <imageobject><imagedata fileref="assign-macro-step1.png" format="PNG"/></imageobject>
+   </mediaobject>
 </figure>
 
-<para>
-Follow the category setup below to better understand this explanation.
-Starting by typing "/k" the input selection will shift from "Tokens" to
-"Keywords" (since K is unique). After that, typing "g" will assign the "good"
-keyword immediately to the image. Typing "b" will show B{ad,oring} in the info
-box and typing "a" or "o" next will complete the match and assign the result to
-the image. Typing "/p" will work similarly and show the partial category match
-"P{eople,laces} so you can type "e" or "l" to complete "People" or "Places"
-respectively.
-<itemizedlist mark='opencircle'>
-    <listitem><para>Tokens</para>
-    <itemizedlist mark='opencircle'>
-        <listitem><para>A..Z</para></listitem>
-    </itemizedlist>
-    </listitem>
-    <listitem>
-    <para>Keywords</para>
-    <itemizedlist mark='opencircle'>
-        <listitem><para>Good</para></listitem>
-        <listitem><para>Bad</para></listitem>
-        <listitem><para>Boring</para></listitem>
-    </itemizedlist>
-    </listitem>
-    <listitem>
-    <para>People</para>
-    <itemizedlist mark='opencircle'>
-        <listitem><para>George</para></listitem>
-        <listitem><para>Fred</para></listitem>
-    </itemizedlist>
-    </listitem>
-    <listitem>
-    <para>Places</para>
-    <itemizedlist mark='opencircle'>
-        <listitem><para>Internet</para></listitem>
-    </itemizedlist>
-    </listitem>
-</itemizedlist>
-</para>
+<para>Next, in the line edit I typed <emphasis>sp</emphasis> which suggest "People / Je<emphasis>sp</emphasis>er" and "People / <emphasis>Sp</emphasis>iff".
+    Both matched the letters I typed. Pressing arrow down to select <emphasis>Spiff</emphasis> and pressing enter, will assign the letter <emphasis>s</emphasis> to adding the tag <emphasis>People / Spiff</emphasis> to the image viewed.
+    This assignment, will be saved for future sessions. To re-assign s to another tag, simply press <keycombo action="simul">&Shift;<keycap>s</keycap></keycombo>.</para>
 
-<sect2>
-    <title>Exact words</title>
-<para>
-    If you want to insert a new word or have words like boa and board you need
-    to be able to type in the exact word you want to insert or select. This can
-    be achieved by starting the word with double quote (") and ending with
-    comma (,). If we would select boa immediately when it is typed you could
-    not select board and otherwise we would be waiting for more key presses and
-    you could not select boa.
-
-<figure id="keyword-exact">
-<title>Selecting Exact Words in Viewer</title>
-<mediaobject>
-<imageobject>
-<imagedata fileref="keyword-exact.png" format="PNG"/>
-</imageobject>
-</mediaobject>
+<figure id="fig-assign-macro-step2">
+  <title>Step 2 - assigning a macro - typing <emphasis>sp</emphasis></title>
+  <mediaobject>
+    <imageobject><imagedata fileref="assign-macro-step2.png" format="PNG"/></imageobject>
+   </mediaobject>
+</figure>
+
+<para>In <xref linkend="fig-assign-macro-overview"/> below you can see existing keybindings in the right side of the dialog.</para>
+<figure id="fig-assign-macro-overview">
+  <title>Overview of assigned key bindings</title>
+  <mediaobject>
+    <imageobject><imagedata fileref="assign-macro-overview.png" format="PNG"/></imageobject>
+   </mediaobject>
 </figure>
-</para>
-</sect2>
-
-<sect2>
-    <title>Always start with category selection</title>
-<para>
-    If you type two '/'s in a row it will toggle between two different modes.
-    The default mode described above and a category selection mode. In the
-    latter mode we go straight back to category selection after a match.
-    That way you can continually select items within different categories.
-    It'll still do the fastest match possible so that typing "kbo" will still
-    match "Keywords/Boring" in the example set above.
-</para>
-</sect2>
-<sect2>
-    <title>Assigning shortcuts</title>
-<para>
-    If the current input is blank (e.g. you're not in the middle of selecting
-    anything) and you hit a function key F1 through F12 (or up to F35 if
-    your keyboard supports), it will assign the last
-    matched assignment to that key. You can apply the same assignment to
-    new images just by hitting the shortcut key. To  remove use shift
-    modifier and the shortcut (<keycombo>&Shift;<keycap>F#</keycap></keycombo>). This is useful for quickly assigning
-    frequently repeating items in your current set of images. It
-    remembers both the category and the category item. These shortcuts are
-    remembered until &kphotoalbum; is closed, there is currently no
-    support for replacing the assigned shortcut.
-    </para>
-  </sect2>
-</sect1>
 
+<para>In case the tag you want to assign doesn't already exist in your database, then simply press the <emphasis>Add New</emphasis> button to create it. This can be seen in <xref linkend="fig-assign-macro-add-new-value.png"/> below</para>
 
-<!-- Drawing on images feature has been dropped
-  <sect1 id="drawing-on-images">
-    <title>Drawing on Images</title>
-    <para>Sometimes it might not be obvious what you want to show with a
-given image, for that purpose &kphotoalbum; allows you to draw on the
-images. </para>
-
-<para>The actual drawing is not saved to the image, but rather to the
-database, that way you can undo your drawing at a later point.</para>
-
-<para>In <xref linkend="fig-draw-on-image"/> you can see an image drawn
-on. To bring &kphotoalbum; in to drawing mode, select <guimenuitem>Draw on
-Image</guimenuitem> from the context menu. To quit drawing, press the cross in
-the toolbar at the top of the window.</para>
-
-<figure id="fig-draw-on-image">
-<title>Drawing on Images</title>
-<mediaobject>
-<imageobject>
-<imagedata fileref="draw-on-image.png" format="PNG"/>
-</imageobject>
-</mediaobject>
+<figure id="fig-assign-macro-add-new-value.png">
+  <title>Adding a new tag</title>
+  <mediaobject>
+    <imageobject><imagedata fileref="assign-macro-add-new-value.png" format="PNG"/></imageobject>
+   </mediaobject>
 </figure>
-  </sect1>
--->
+
+<para>While tagging images from the viewer, you may want to add a tag, without binding it to a key. To do so simply press <keycombo>&Ctrl;<keycap>a</keycap></keycombo></para>
+
+</sect1>
 </chapter>
 
 <!-- Keep this comment at the end of the file


More information about the kde-doc-english mailing list