From ab2d3b7577a6fc1606c67f92befc309f739be99f Mon Sep 17 00:00:00 2001
From: Philippe Mavridis <philippe.mavridis@yandex.com>
Date: Wed, 1 Apr 2026 18:59:40 +0300
Subject: KCharSelect: add categories dock

The category dock provides an easier way to navigate the Unicode tables compared to entering the table number or manually scrolling until the desired table. The category names are officially defined by the Unicode standard and are part of many character pickers nowadays.

The proposed dock provides a sortable and searchable hierarchy of categories and subcategories, can be moved, resized or hidden and the settings are preserved at the next start.

Signed-off-by: Philippe Mavridis <philippe.mavridis@yandex.com>
Co-authored-by: Opencode/Big Pickle
---
 kcharselect/CMakeLists.txt     |   2 +-
 kcharselect/kcharselectdia.cpp | 218 ++++++++++++++++++++++++++++++++++++++++-
 kcharselect/kcharselectdia.h   |  21 +++-
 kcharselect/kcharselectui.rc   |   8 +-
 4 files changed, 237 insertions(+), 12 deletions(-)

diff --git a/kcharselect/CMakeLists.txt b/kcharselect/CMakeLists.txt
index 1ccd7507..abbd5311 100644
--- a/kcharselect/CMakeLists.txt
+++ b/kcharselect/CMakeLists.txt
@@ -27,7 +27,7 @@ link_directories(
 tde_add_executable( kcharselect AUTOMOC
   SOURCES kcharselectdia.cpp main.cpp
   LINK tdeui-shared
-  DESTINATION ${BIN_INSTALL_DIR} 
+  DESTINATION ${BIN_INSTALL_DIR}
 )
 
 
diff --git a/kcharselect/kcharselectdia.cpp b/kcharselect/kcharselectdia.cpp
index d5232a83..b7b62d8a 100644
--- a/kcharselect/kcharselectdia.cpp
+++ b/kcharselect/kcharselectdia.cpp
@@ -9,17 +9,31 @@
 
 #include <stdlib.h>
 
+#include <tqcursor.h>
+#include <tqtimer.h>
+
 #include <kdialog.h>
 #include <tdeapplication.h>
 #include <tdeaccel.h>
 #include <tdeconfig.h>
+#include <tdelistview.h>
+#include <tdelistviewsearchline.h>
 #include <tdelocale.h>
 #include <tdeaction.h>
+#include <tdeunicodedata.h>
 
 /******************************************************************/
 /* class KCharSelectDia                                           */
 /******************************************************************/
 
+// Surrogate code points (U+D800 - U+DFFF) are used to encode characters
+// outside the Basic Multilingual Plane and are not characters on their own,
+// so there is no point in listing them in the category browser
+static constexpr bool isSurrogate(char32_t codepoint)
+{
+    return codepoint >= 0xd800 && codepoint <= 0xdfff;
+}
+
 //==================================================================
 KCharSelectDia::KCharSelectDia(TQWidget *parent,const char *name,
 			       const TQChar &_chr,const TQString &_font,
@@ -44,6 +58,62 @@ KCharSelectDia::KCharSelectDia(TQWidget *parent,const char *name,
 	  TQ_SLOT(fontSelected(const TQString &)));
   grid->addMultiCellWidget(charSelect, 0, 0, 0, 3);
 
+  // Add category browser dock widget
+  categoryDock = new TQDockWindow(this, "categories");
+
+  TQVBox *cvbox = new TQVBox(categoryDock);
+
+  categoryLV = new TDEListView(cvbox, "dock_categories");
+  categoryLV->addColumn(i18n("Code point"));
+  categoryLV->addColumn(i18n("Name"));
+  categoryLV->setRootIsDecorated(true);
+  categoryLV->setResizeMode(TQListView::LastColumn);
+  categoryLV->setAllColumnsShowFocus(true);
+  connect(categoryLV, TQ_SIGNAL(currentChanged(TQListViewItem*)), this, TQ_SLOT(categorySelected(TQListViewItem*)));
+
+  TDEListViewSearchLineWidget *lvsw = new TDEListViewSearchLineWidget(categoryLV, cvbox);
+
+  categoryDock->setWidget(cvbox);
+  categoryDock->setCaption(i18n("Categories"));
+  categoryDock->setHorizontallyStretchable(true);
+  categoryDock->setVerticallyStretchable(true);
+  categoryDock->setResizeEnabled(true);
+  categoryDock->setCloseMode(TQDockWindow::Always);
+
+  // BUG TDEMainWindow::saveMainWindowSettings() does not save TQDockWindow settings
+  TDEConfig *config = tdeApp->config();
+  config->setGroup("Docks");
+  TQt::Dock categoriesDock = (TQt::Dock)config->readNumEntry("Categories dock", TQt::DockLeft);
+  int categoriesIndex      = config->readNumEntry("Categories index", 0);
+  bool categoriesNewline   = config->readBoolEntry("Categories nl", false);
+  int categoriesOffset     = config->readNumEntry("Categories offset", 0);
+  int categoriesSize       = config->readNumEntry("Categories size", -1);
+  bool categoriesShown     = config->readBoolEntry("Categories shown", true);
+
+  addDockWindow(categoryDock, i18n("Categories"), categoriesDock);
+  moveDockWindow(categoryDock, categoriesDock, categoriesNewline, categoriesIndex, categoriesOffset);
+  if (categoriesSize >= 0)
+  {
+      if (categoryDock->orientation() == TQt::Horizontal)
+      {
+          categoryDock->setFixedExtentHeight(categoriesSize);
+      }
+      else
+      {
+          categoryDock->setFixedExtentWidth(categoriesSize);
+      }
+  }
+
+  TDEToggleAction *tCategories = new TDEToggleAction(i18n("Show categories dock"), 0,
+        this, TQ_SLOT(toggleCategoriesDock()), actionCollection(), "show_categories");
+
+  tCategories->setChecked(categoriesShown);
+  categoryDock->setShown(categoriesShown);
+
+  connect(categoryDock, TQ_SIGNAL(visibilityChanged(bool)), tCategories, TQ_SLOT(setChecked(bool)));
+
+  TQTimer::singleShot(0, this, TQ_SLOT(populateCategories()));
+
   // Build line editor
   lined = new TQLineEdit(mainWidget);
   lined->resize(lined->sizeHint());
@@ -78,7 +148,7 @@ KCharSelectDia::KCharSelectDia(TQWidget *parent,const char *name,
 
   // Build menu
   KStdAction::quit( this, TQ_SLOT(_exit()), actionCollection() );
-  
+
   new TDEAction(i18n("&To Clipboard"), "edit-copy",
          TDEStdAccel::shortcut(TDEStdAccel::Copy), this, TQ_SLOT(toClip()), actionCollection(), "copy_clip" );
 
@@ -86,20 +156,20 @@ KCharSelectDia::KCharSelectDia(TQWidget *parent,const char *name,
     TQ_SLOT(toClipUTF8()), actionCollection(), "copy_utf_8" );
   (void)new TDEAction(i18n("To Clipboard &HTML"), 0, this,
       TQ_SLOT(toClipHTML()), actionCollection(), "copy_html" );
- 
+
   new TDEAction(i18n("&From Clipboard"), "edit-paste",
          TDEStdAccel::shortcut(TDEStdAccel::Paste), this, TQ_SLOT(fromClip()), actionCollection(), "from_clip" );
   (void)new TDEAction(i18n("From Clipboard UTF-8"), 0, this,
       TQ_SLOT(fromClipUTF8()), actionCollection(), "from_clip_utf8" );
-  
+
   i18n("From Clipboard HTML");      // Intended for future use
-  
+
   KStdAction::clear(this, TQ_SLOT(clear()), actionCollection(), "clear");
   (void)new TDEAction(i18n("&Flip"), 0, this,
       TQ_SLOT(flipText()), actionCollection(), "flip" );
   (void)new TDEAction(i18n("&Alignment"), 0, this,
       TQ_SLOT(toggleEntryDirection()), actionCollection(), "alignment" );
-  
+
   charSelect->setFocus();
 
   entryDirection = direction;
@@ -114,7 +184,9 @@ KCharSelectDia::KCharSelectDia(TQWidget *parent,const char *name,
 //==================================================================
 void KCharSelectDia::charChanged(const TQChar &_chr)
 {
+  if (vChr == _chr) return;
   vChr = _chr;
+  updateCurrentCategory();
 }
 
 //==================================================================
@@ -269,6 +341,27 @@ void KCharSelectDia::_exit()
   config->writeEntry("char",static_cast<int>(vChr.unicode()));
   config->writeEntry("table",charSelect->tableNum());
   config->writeEntry("entryDirection",entryDirection);
+
+  // BUG TDEMainWindow::saveMainWindowSettings() does not save TQDockWindow settings
+  TQt::Dock dock; int index, offset; bool nl;
+  if (getLocation(categoryDock, dock, index, nl, offset))
+  {
+    config->setGroup("Docks");
+    config->writeEntry("Categories dock", dock);
+    config->writeEntry("Categories index", index);
+    config->writeEntry("Categories nl", nl);
+    config->writeEntry("Categories offset", offset);
+    config->writeEntry("Categories shown", categoryDock->isShown());
+
+    if (categoryDock->orientation() == TQt::Horizontal)
+    {
+      config->writeEntry("Categories size", categoryDock->height());
+    }
+    else
+    {
+      config->writeEntry("Categories size", categoryDock->width());
+    }
+  }
   config->sync();
 
   delete this;
@@ -287,3 +380,118 @@ void KCharSelectDia::help()
   tdeApp->invokeHelp();
 }
 
+//==================================================================
+void KCharSelectDia::populateCategories()
+{
+    categoryLV->setCursor(TQCursor(TQt::BusyCursor));
+    categoryLV->setEnabled(false);
+
+    // Give the busy cursor a chance to be painted before filling the list
+    tqApp->processEvents();
+
+    const bool updatesState = categoryLV->isUpdatesEnabled();
+    const bool signalsState = categoryLV->signalsBlocked();
+    categoryLV->setUpdatesEnabled(false);
+    // Signals are blocked to prevent categorySelected() from being called for
+    // every single item added below; the previous state is restored afterwards.
+    categoryLV->blockSignals(true);
+
+    TQString ucStr;
+    TDEUnicodeData *ud = TDEUnicodeData::instance();
+    const TDEUnicodeData::Category *categoriesData = ud->categoryList();
+    const size_t categoriesCount = ud->categoryCount();
+
+    for (size_t i = 0; i < categoriesCount; ++i)
+    {
+        const TDEUnicodeData::Category *category = &categoriesData[i];
+
+        // TQt only supports Unicode codepoints up to 0xffff (BUG TDE/tdelibs#399)
+        if (category->index > 0xffff) break;
+
+        if (isSurrogate(category->index)) continue;
+
+        ucStr.sprintf("%04x", static_cast<int>(category->index));
+        TQListViewItem *categoryItem = new TQListViewItem(categoryLV, ucStr, i18n(category->name));
+        mCategoryLVItems[category->index] = categoryItem;
+
+        for (size_t j = 0; j < category->subcategories_size; ++j)
+        {
+            const TDEUnicodeData::Subcategory *subcategory = &category->subcategories[j];
+
+            // TQt only supports Unicode codepoints up to 0xffff (BUG TDE/tdelibs#399)
+            if (subcategory->index > 0xffff) break;
+
+            if (isSurrogate(subcategory->index)) continue;
+
+            ucStr.sprintf("%04x", static_cast<int>(subcategory->index));
+            mSubcategoryLVItems[subcategory->index] = new TQListViewItem(categoryItem, ucStr, i18n(subcategory->name));
+        }
+    }
+
+    categoryLV->blockSignals(signalsState);
+    categoryLV->setUpdatesEnabled(updatesState);
+    categoryLV->setEnabled(true);
+    categoryLV->setCursor(TQCursor(TQt::ArrowCursor));
+    categoryLV->update();
+
+    updateCurrentCategory();
+}
+
+//==================================================================
+void KCharSelectDia::categorySelected(TQListViewItem *category)
+{
+    bool ok;
+    const int uc = category->text(0).toInt(&ok, 16);
+
+    // TQt only supports Unicode codepoints up to 0xffff (BUG TDE/tdelibs#399)
+    if (!ok || uc > 0xffff) return;
+
+    const int table = uc / 256;
+    charSelect->setTableNum(table);
+
+    const TQChar ch(uc);
+    charSelect->setChar(ch);
+}
+
+//==================================================================
+// Updates category dock to reflect the category of the currently selected character
+void KCharSelectDia::updateCurrentCategory()
+{
+    const char32_t uc = vChr.unicode();
+
+    // TQt only supports Unicode codepoints up to 0xffff (BUG TDE/tdelibs#399)
+    if (uc > 0xffff) return;
+
+    TDEUnicodeData *ud = TDEUnicodeData::instance();
+
+    const TDEUnicodeData::Category *category = ud->findCategory(uc);
+    if (!category) return;
+
+    // Prefer the more specific subcategory. findSubcategory() returns nullptr
+    // for reserved gaps at the start of a block (e.g. Tamil starts at 0x0B80
+    // but its first subcategory starts at 0x0B82), in which case fall back to
+    // the block item so a codepoint in that gap is not mis-attributed.
+    const TDEUnicodeData::Subcategory *subcategory = ud->findSubcategory(category, uc);
+    const bool hasSubcategory = (subcategory != nullptr);
+    const char32_t index = hasSubcategory ? subcategory->index : category->index;
+    TQMap<char32_t, TQListViewItem*> *itemMap = hasSubcategory ? &mSubcategoryLVItems : &mCategoryLVItems;
+
+    const TQMap<char32_t, TQListViewItem*>::Iterator it = itemMap->find(index);
+    if (it == itemMap->end()) return;
+
+    TQListViewItem *lvi = it.data();
+
+    // We block signals to prevent categorySelected() from being called, and
+    // restore the previous blocked-state afterwards.
+    bool areSignalsBlocked = categoryLV->signalsBlocked();
+    categoryLV->blockSignals(true);
+    categoryLV->setCurrentItem(lvi);
+    categoryLV->ensureItemVisible(lvi);
+    categoryLV->blockSignals(areSignalsBlocked);
+}
+
+//==================================================================
+void KCharSelectDia::toggleCategoriesDock()
+{
+    categoryDock->setShown(static_cast<const TDEToggleAction*>(TQObject::sender())->isChecked());
+}
diff --git a/kcharselect/kcharselectdia.h b/kcharselect/kcharselectdia.h
index c2b37423..4a76e050 100644
--- a/kcharselect/kcharselectdia.h
+++ b/kcharselect/kcharselectdia.h
@@ -7,16 +7,22 @@
 #ifndef kcharselectdia_h
 #define kcharselectdia_h
 
+#include <map>
 
 #include <tqlayout.h>
 #include <tqlineedit.h>
 #include <tqclipboard.h>
+#include <tqpair.h>
 
 #include <kpushbutton.h>
 #include <kcharselect.h>
 #include <tdemainwindow.h>
 
-static const char *version = "v1.1"; 
+static const char *version = "v1.1";
+
+class TQDockWindow;
+class TDEListView;
+class TQListViewItem;
 
 /******************************************************************/
 /* class KCharSelectDia                                           */
@@ -25,7 +31,7 @@ static const char *version = "v1.1";
 class KCharSelectDia : public TDEMainWindow
 {
   TQ_OBJECT
-  
+
 
 public:
   KCharSelectDia(TQWidget *parent,const char *name,const TQChar &_chr,const TQString &_font,int _tableNum, bool direction);
@@ -43,17 +49,20 @@ protected:
   TQLineEdit   *lined;
   KPushButton *bClip,*bClear;
   KPushButton *bHelp;
+  TQDockWindow *categoryDock;
+  TDEListView *categoryLV;
 
   TQChar vChr;
   TQString vFont;
   int pointSize;
   bool entryDirection;
+  TQMap<char32_t, TQListViewItem*> mCategoryLVItems;
+  TQMap<char32_t, TQListViewItem*> mSubcategoryLVItems;
 
 protected slots:
   void charChanged(const TQChar &_chr);
   void fontSelected(const TQString &_font);
-  void add()
-  { add(vChr); }
+  void add() { add(vChr); }
   void add(const TQChar &_chr);
   void toClip();
   void toClipUTF8();
@@ -66,6 +75,10 @@ protected slots:
   void _exit();
   void clear();
   void help();
+  void populateCategories();
+  void categorySelected(TQListViewItem*);
+  void updateCurrentCategory();
+  void toggleCategoriesDock();
 
 };
 
diff --git a/kcharselect/kcharselectui.rc b/kcharselect/kcharselectui.rc
index 77682937..299ec31d 100644
--- a/kcharselect/kcharselectui.rc
+++ b/kcharselect/kcharselectui.rc
@@ -1,5 +1,5 @@
 <!DOCTYPE kpartgui>
-<kpartgui name="kcharselect" version="1">
+<kpartgui name="kcharselect" version="2">
 <MenuBar>
   <Menu name="edit"><text>&amp;Edit</text>
     <Action name="copy_clip"/>
@@ -11,6 +11,10 @@
 		<Action name="clear"/>
     <Action name="flip"/>
     <Action name="alignment"/>
- </Menu>
+  </Menu>
+  <Menu name="settings"><text>&amp;Settings</text>
+    <Action name="show_categories"/>
+    <Separator/>
+  </Menu>
 </MenuBar>
 </kpartgui>
-- 
cgit v1.2.3

