From 0dfed4cee5fd6cc7ec64b7d3f7ee2923e92b229b Mon Sep 17 00:00:00 2001 From: "Peter P. Lupo" Date: Tue, 16 Jun 2026 11:04:29 -0400 Subject: [PATCH] wlx/structview: Resolve PR conflicts by clean rebuild off upstream/master --- sdk/wlxplugin.h | 1 + wlx/structview/.gitignore | 1 + wlx/structview/CMakeLists.txt | 71 + wlx/structview/README.md | 95 ++ wlx/structview/include/StructViewWidget.h | 110 ++ wlx/structview/include/TextFormatEngine.h | 103 ++ wlx/structview/src/CborEngine.cpp | 86 ++ wlx/structview/src/IniEngine.cpp | 148 ++ wlx/structview/src/JsonEngine.cpp | 378 +++++ wlx/structview/src/StructViewWidget.cpp | 655 +++++++++ wlx/structview/src/TomlEngine.cpp | 261 ++++ wlx/structview/src/XmlEngine.cpp | 265 ++++ wlx/structview/src/YamlEngine.cpp | 239 ++++ wlx/structview/src/wlx_entry.cpp | 164 +++ wlx/structview/structview_cbor_edit.png | Bin 0 -> 42598 bytes wlx/structview/structview_json.png | Bin 0 -> 75605 bytes wlx/structview/structview_xml.png | Bin 0 -> 89935 bytes wlx/wlxbase_wlqt/CMakeLists.txt | 82 ++ wlx/wlxbase_wlqt/README.md | 810 +++++++++++ .../include/wlxbase_wlqt/CrashLogger.h | 74 + .../include/wlxbase_wlqt/EditableGridWidget.h | 154 +++ .../include/wlxbase_wlqt/EncodingUtils.h | 40 + .../include/wlxbase_wlqt/FilterRowWidget.h | 48 + .../wlxbase_wlqt/FilterableHeaderView.h | 61 + .../include/wlxbase_wlqt/FindReplacePanel.h | 74 + .../include/wlxbase_wlqt/FocusManager.h | 98 ++ .../include/wlxbase_wlqt/PluginSplitView.h | 26 + .../include/wlxbase_wlqt/PluginStatusBar.h | 40 + .../include/wlxbase_wlqt/PluginToolBar.h | 48 + .../wlxbase_wlqt/ScopedFindReplacePanel.h | 25 + .../wlxbase_wlqt/SequentialRowProxyModel.h | 16 + .../include/wlxbase_wlqt/ThemeManager.h | 26 + wlx/wlxbase_wlqt/src/CrashLogger.cpp | 193 +++ wlx/wlxbase_wlqt/src/EditableGridWidget.cpp | 1216 +++++++++++++++++ wlx/wlxbase_wlqt/src/EncodingUtils.cpp | 122 ++ wlx/wlxbase_wlqt/src/FilterRowWidget.cpp | 102 ++ wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp | 173 +++ wlx/wlxbase_wlqt/src/FindReplacePanel.cpp | 151 ++ wlx/wlxbase_wlqt/src/FocusManager.cpp | 332 +++++ wlx/wlxbase_wlqt/src/PluginSplitView.cpp | 34 + wlx/wlxbase_wlqt/src/PluginStatusBar.cpp | 136 ++ wlx/wlxbase_wlqt/src/PluginToolBar.cpp | 160 +++ .../src/ScopedFindReplacePanel.cpp | 34 + .../src/SequentialRowProxyModel.cpp | 22 + wlx/wlxbase_wlqt/src/ThemeManager.cpp | 282 ++++ 45 files changed, 7156 insertions(+) create mode 100644 wlx/structview/.gitignore create mode 100644 wlx/structview/CMakeLists.txt create mode 100644 wlx/structview/README.md create mode 100644 wlx/structview/include/StructViewWidget.h create mode 100644 wlx/structview/include/TextFormatEngine.h create mode 100644 wlx/structview/src/CborEngine.cpp create mode 100644 wlx/structview/src/IniEngine.cpp create mode 100644 wlx/structview/src/JsonEngine.cpp create mode 100644 wlx/structview/src/StructViewWidget.cpp create mode 100644 wlx/structview/src/TomlEngine.cpp create mode 100644 wlx/structview/src/XmlEngine.cpp create mode 100644 wlx/structview/src/YamlEngine.cpp create mode 100644 wlx/structview/src/wlx_entry.cpp create mode 100644 wlx/structview/structview_cbor_edit.png create mode 100644 wlx/structview/structview_json.png create mode 100644 wlx/structview/structview_xml.png create mode 100644 wlx/wlxbase_wlqt/CMakeLists.txt create mode 100644 wlx/wlxbase_wlqt/README.md create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h create mode 100644 wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h create mode 100644 wlx/wlxbase_wlqt/src/CrashLogger.cpp create mode 100644 wlx/wlxbase_wlqt/src/EditableGridWidget.cpp create mode 100644 wlx/wlxbase_wlqt/src/EncodingUtils.cpp create mode 100644 wlx/wlxbase_wlqt/src/FilterRowWidget.cpp create mode 100644 wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp create mode 100644 wlx/wlxbase_wlqt/src/FindReplacePanel.cpp create mode 100644 wlx/wlxbase_wlqt/src/FocusManager.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginSplitView.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginStatusBar.cpp create mode 100644 wlx/wlxbase_wlqt/src/PluginToolBar.cpp create mode 100644 wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp create mode 100644 wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp create mode 100644 wlx/wlxbase_wlqt/src/ThemeManager.cpp diff --git a/sdk/wlxplugin.h b/sdk/wlxplugin.h index 8f33fb2..eebce8e 100644 --- a/sdk/wlxplugin.h +++ b/sdk/wlxplugin.h @@ -9,6 +9,7 @@ #define lc_newparams 2 #define lc_selectall 3 #define lc_setpercent 4 +#define lc_focus 5 #define lcp_wraptext 1 #define lcp_fittowindow 2 diff --git a/wlx/structview/.gitignore b/wlx/structview/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/wlx/structview/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/wlx/structview/CMakeLists.txt b/wlx/structview/CMakeLists.txt new file mode 100644 index 0000000..83f98b0 --- /dev/null +++ b/wlx/structview/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.20) +project(structview_qt6 CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +# Allow older cmake_minimum_required in FetchContent dependencies +set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Xml) +# Ensure the platform static library is built with -fPIC +# (required because we link it into a shared .wlx library) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +include(FetchContent) + +# --------------------------------------------------------------------------- +# yaml-cpp — YAML 1.2 parser (using system package) +# --------------------------------------------------------------------------- +find_package(yaml-cpp REQUIRED) + +# --------------------------------------------------------------------------- +# toml++ — header-only TOML parser (v3.4.0) +# --------------------------------------------------------------------------- +FetchContent_Declare(tomlplusplus + GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git + GIT_TAG v3.4.0 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(tomlplusplus) + +# Pull in the platform library (builds libwlxbase_wlqt.a) +add_subdirectory( + ${CMAKE_CURRENT_SOURCE_DIR}/../wlxbase_wlqt + ${CMAKE_CURRENT_BINARY_DIR}/wlxbase_wlqt +) + +# NOTE: Headers with Q_OBJECT must be listed explicitly so AUTOMOC processes them. +add_library(structview_qt6 SHARED + include/StructViewWidget.h + include/TextFormatEngine.h + src/wlx_entry.cpp + src/StructViewWidget.cpp + src/JsonEngine.cpp + src/XmlEngine.cpp + src/IniEngine.cpp + src/CborEngine.cpp + src/YamlEngine.cpp + src/TomlEngine.cpp +) + +set_target_properties(structview_qt6 PROPERTIES + PREFIX "" + SUFFIX ".wlx" +) + +target_include_directories(structview_qt6 PRIVATE + include + ${CMAKE_CURRENT_SOURCE_DIR}/../../sdk +) + +target_link_libraries(structview_qt6 PRIVATE + wlxbase_wlqt + Qt6::Widgets + Qt6::Gui + Qt6::Core + Qt6::Xml + yaml-cpp + tomlplusplus::tomlplusplus +) diff --git a/wlx/structview/README.md b/wlx/structview/README.md new file mode 100644 index 0000000..e2239e5 --- /dev/null +++ b/wlx/structview/README.md @@ -0,0 +1,95 @@ +# structview — Unified Structured Text Plugin + +A Qt6 WLX plugin for [Double Commander](https://doublecmd.github.io/) that views and edits structured text files: **JSON**, **XML**, **INI**, **CBOR**, **TOML**, and **YAML**. + +Built on the [`wayland_qt_base`](../../plugin-platform/wlx/wayland_qt_base/) platform library. + +## Features + +- **JSON & CBOR**: Flattens top-level arrays of objects into a grid. Columns = union of all keys. Nested values shown as compact JSON. Full roundtrip serialization preserving types. +- **XML**: Auto-detects repeating child elements as rows. Attributes shown as `@attr` columns. Non-tabular XML falls back to Name/Value layout. +- **TOML & YAML**: Full parsing, editing, and roundtrip serialization of nested structures. +- **INI**: Section navigation list on the left, 2-column Key/Value grid on the right. Sections switch without losing edits. +- **Copy Path & Subtree**: Right-click on tree nodes or grid cells to copy the JSONPath (for JSON, CBOR, TOML, YAML, INI) or XPath (1-based index, for XML), or copy the serialized subtree/key-value pair. +- **Automatic System Theme**: Auto-detects system's light/dark preference, defaulting to dark theme if undetected. +- **Find & Replace** with scope filtering (All Cells, Current Column, Current Row) +- **Full undo/redo** (Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y) via `GridMode::MemoryDocument` +- **Save** (Ctrl+S) writes back to the original file +- **Word wrap** and **grid lines** toggles +- **Encoding detection** via enca — auto-converts non-UTF-8 files + +## Architecture + +``` +structview_qt6.wlx (shared library) + ├── wlx_entry.cpp → WLX C interface (ListLoad, etc.) + ├── StructViewWidget → Main widget assembly + ├── TextFormatEngine → Abstract parser base + factory + │ ├── JsonEngine → QJsonDocument + │ ├── XmlEngine → QDomDocument + │ └── IniEngine → QSettings (section-navigated) + └── wayland_qt_base (static library via submodule) + ├── FocusManager + ├── EditableGridWidget (GridMode::MemoryDocument) + ├── PluginToolBar + ├── ScopedFindReplacePanel + └── EncodingUtils +``` + +### Adding a New Format + +1. Create `src/NewFormatEngine.cpp` with a class inheriting `TextFormatEngine` +2. Implement `loadInto()`, `serialize()`, `formatName()` +3. Add a factory function `createNewFormatEngine()` and wire it in the factory switch in `IniEngine.cpp` +4. Add the source file to `CMakeLists.txt` + +## Building + +```bash +cd wlx/structview +mkdir build && cd build +cmake .. +make -j$(nproc) +``` + +**Requirements:** +- Qt6 (Core, Gui, Widgets, Xml) +- CMake ≥ 3.20 +- The `plugin-platform` submodule (auto-fetched via `git submodule update --init`) + +**Output:** `structview_qt6.wlx` + +## Installation + +Copy `structview_qt6.wlx` to your Double Commander plugins directory and configure the detect string: + +``` +EXT="JSON" | EXT="XML" | EXT="INI" +``` + +## Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| Ctrl+S | Save file | +| Ctrl+Z | Undo | +| Ctrl+Shift+Z / Ctrl+Y | Redo | +| Ctrl+F | Toggle Find/Replace panel | +| Ctrl+C | Copy selection | +| Ctrl+V | Paste | + +## Future + +- Additional structured text formats +- Enhanced grid editing performance on very large files + +## Screenshots + +### JSON Viewer +![JSON Viewer](structview_json.png) + +### XML Viewer +![XML Viewer](structview_xml.png) + +### CBOR Editor +![CBOR Editor](structview_cbor_edit.png) diff --git a/wlx/structview/include/StructViewWidget.h b/wlx/structview/include/StructViewWidget.h new file mode 100644 index 0000000..1e46ab6 --- /dev/null +++ b/wlx/structview/include/StructViewWidget.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "TextFormatEngine.h" + +namespace QtWlPlugin { +class FocusManager; +class PluginToolBar; +class EditableGridWidget; +class ScopedFindReplacePanel; +class FilterableHeaderView; +class PluginStatusBar; +class PluginSplitView; +class SequentialRowProxyModel; +} + +/// Main plugin widget for structured text file viewing/editing. +/// +/// Layout: +/// ┌────────────────────────────────────────────┐ +/// │ PluginToolBar │ +/// ├────────────┬───────────────────────────────┤ +/// │ QTreeView │ QTabWidget (Grid | Text) │ +/// │ (document │ ┌ FilterableHeaderView │ +/// │ tree) │ ├ QTableView (grid) │ +/// │ │ │ │ +/// ├────────────┴──┴────────────────────────────┤ +/// │ PluginStatusBar │ +/// └────────────────────────────────────────────┘ +class QLabel; + +/// Main plugin widget for structured text file viewing/editing. +/// +/// Layout: +/// ┌────────────────────────────────────────────┐ +/// │ PluginToolBar │ +/// ├────────────┬───────────────────────────────┤ +/// │ QTreeView │ QTabWidget (Grid | Text) │ +/// │ (document │ ┌ FilterableHeaderView │ +/// │ tree) │ ├ QTableView (grid) │ +/// │ │ │ │ +/// ├────────────┴──┴────────────────────────────┤ +/// │ PluginStatusBar │ +/// └────────────────────────────────────────────┘ +class StructViewWidget : public QWidget { + Q_OBJECT +public: + explicit StructViewWidget(QWidget *parent = nullptr); + ~StructViewWidget() override; + + bool loadFile(const QString &filepath); + bool saveFile(); + bool saveFileAs(const QString &path); + + // WLX bridge accessors + QtWlPlugin::FocusManager *focusManager() const; + QtWlPlugin::EditableGridWidget *grid() const; + QString getSelectionAsText(char sep = '\t'); + +private slots: + void onSave(); + void onFind(bool forward); + void onTreeNodeSelected(const QModelIndex ¤t, const QModelIndex &previous); + void showTreeContextMenu(const QPoint &pos); + +private: + void setupUi(); + void setupToolbar(); + void setupFindReplace(); + void populateTree(); + void populateTreeNode(QStandardItem *parentItem, DocumentNode *node); + void showNodeData(DocumentNode *node); + void updateStatusBar(); + + QString getJsonPath(const DocumentNode *node) const; + QString getXmlPath(const DocumentNode *node) const; + QString getCellPath(const DocumentNode *node, int r, int c) const; + + QString m_filepath; + std::unique_ptr m_engine; + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; + QtWlPlugin::ScopedFindReplacePanel *m_findReplace; + QtWlPlugin::PluginStatusBar *m_statusBar; + QLabel *m_dirtyIndicator = nullptr; + + // Left panel: document tree + QtWlPlugin::PluginSplitView *m_splitView; + QTreeView *m_treeView; + QStandardItemModel *m_treeModel; + + // Right panel: Grid + Text tabs + QTabWidget *m_tabWidget; + QtWlPlugin::EditableGridWidget *m_grid; + QTableView *m_gridView; + QStandardItemModel *m_gridModel; + QtWlPlugin::SequentialRowProxyModel *m_filterProxy; + QtWlPlugin::FilterableHeaderView *m_filterHeader; + QPlainTextEdit *m_textView; + + DocumentNode *m_currentNode = nullptr; +}; diff --git a/wlx/structview/include/TextFormatEngine.h b/wlx/structview/include/TextFormatEngine.h new file mode 100644 index 0000000..99fe6fa --- /dev/null +++ b/wlx/structview/include/TextFormatEngine.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/// A node in the document tree. +/// +/// Each engine builds a tree of DocumentNode objects representing the +/// hierarchical structure of the file. The StructViewWidget displays +/// this tree in a QTreeView (left panel) and shows the selected node's +/// grid data in a QTableView (right panel). +/// +/// Grid data (columnNames + rows) represents this node's immediate children +/// that are displayable as a table. Container children become tree nodes +/// instead of grid rows. +class DocumentNode { +public: + explicit DocumentNode(const QString &name, DocumentNode *parent = nullptr) + : name(name), parent(parent) {} + + ~DocumentNode() { qDeleteAll(children); } + + QString name; ///< Display name in tree + DocumentNode *parent = nullptr; + QList children; + + // Grid data for this node + QStringList columnNames; ///< Column headers + QVector> rows; ///< Row data + + bool editable = true; ///< Whether grid data can be edited + + /// Add a child node and return pointer to it. + DocumentNode *addChild(const QString &childName) { + auto *child = new DocumentNode(childName, this); + children.append(child); + return child; + } + + /// Remove and delete a child by index. + void removeChild(int index) { + if (index >= 0 && index < children.size()) { + delete children.takeAt(index); + } + } + + /// Position in parent's children list, or -1 if root. + int childIndex() const { + if (!parent) return -1; + return parent->children.indexOf(const_cast(this)); + } + + /// True if node has no children and no grid data. + bool isLeaf() const { + return children.isEmpty() && rows.isEmpty(); + } + + /// True if node has child nodes (not just grid rows). + bool isContainer() const { + return !children.isEmpty(); + } +}; + +/// Abstract base class for structured text format engines. +/// +/// Each engine: +/// 1. Parses raw file bytes into a DocumentNode tree +/// 2. Provides raw text for the read-only Text tab +/// 3. Serializes the tree back to bytes for saving +/// +/// The factory method createForFile() inspects the file extension and +/// returns the appropriate engine. +class TextFormatEngine { +public: + virtual ~TextFormatEngine() = default; + + /// Parse raw bytes and build the document tree. + virtual bool parse(const QByteArray &data) = 0; + + /// Root of the parsed document tree. + virtual DocumentNode *rootNode() const = 0; + + /// Serialize the entire tree back to bytes for saving. + /// Must reconstruct from tree structure + any grid edits. + virtual QByteArray serialize() const = 0; + + /// Serialize a specific subtree to bytes. + virtual QByteArray serializeSubtree(const DocumentNode *node) const = 0; + + /// Pretty-printed raw text for the read-only Text tab. + virtual QString rawText() const = 0; + + /// Human-readable format name (e.g. "JSON", "XML"). + virtual QString formatName() const = 0; + + /// Factory: detect format from file extension and return the right engine. + static std::unique_ptr createForFile(const QString &filepath); +}; diff --git a/wlx/structview/src/CborEngine.cpp b/wlx/structview/src/CborEngine.cpp new file mode 100644 index 0000000..b9927b3 --- /dev/null +++ b/wlx/structview/src/CborEngine.cpp @@ -0,0 +1,86 @@ +#include "TextFormatEngine.h" + +#include +#include +#include +#include +#include +#include +#include + +// Forward declare factory +std::unique_ptr createJsonEngine(); + +/// CBOR engine: binary JSON format. +/// +/// Converts CBOR → JSON → delegates to JsonEngine's tree builder. +/// Serializes back via JSON → CBOR. +class CborEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_jsonEngine ? m_jsonEngine->rootNode() : nullptr; } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("CBOR"); } + +private: + std::unique_ptr m_jsonEngine; + QString m_rawText; +}; + +bool CborEngine::parse(const QByteArray &data) +{ + QCborParserError err; + QCborValue cbor = QCborValue::fromCbor(data, &err); + if (err.error != QCborError::NoError) + return false; + + // Convert to JSON for tree building and text display + QJsonValue jsonVal = cbor.toJsonValue(); + QJsonDocument doc; + if (jsonVal.isObject()) + doc = QJsonDocument(jsonVal.toObject()); + else if (jsonVal.isArray()) + doc = QJsonDocument(jsonVal.toArray()); + else + return false; + + m_rawText = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + + // Delegate to JsonEngine: parse the JSON bytes + m_jsonEngine = createJsonEngine(); + QByteArray jsonBytes = doc.toJson(QJsonDocument::Compact); + return m_jsonEngine->parse(jsonBytes); +} + +QByteArray CborEngine::serialize() const +{ + if (!m_jsonEngine) return {}; + + // Get JSON bytes from the JSON engine + QByteArray jsonBytes = m_jsonEngine->serialize(); + + // Convert JSON → CBOR + QJsonDocument doc = QJsonDocument::fromJson(jsonBytes); + QCborValue cbor; + if (doc.isObject()) + cbor = QCborValue::fromJsonValue(QJsonValue(doc.object())); + else if (doc.isArray()) + cbor = QCborValue::fromJsonValue(QJsonValue(doc.array())); + else + return {}; + + return cbor.toCbor(); +} + +QByteArray CborEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!m_jsonEngine) return {}; + return m_jsonEngine->serializeSubtree(node); +} + +std::unique_ptr createCborEngine() +{ + return std::make_unique(); +} diff --git a/wlx/structview/src/IniEngine.cpp b/wlx/structview/src/IniEngine.cpp new file mode 100644 index 0000000..4fd0a03 --- /dev/null +++ b/wlx/structview/src/IniEngine.cpp @@ -0,0 +1,148 @@ +#include "TextFormatEngine.h" + +#include +#include +#include + +/// INI engine: sections as tree nodes, keys as 2-column grid. +class IniEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_root.get(); } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("INI"); } + +private: + std::unique_ptr m_root; + QString m_rawText; + QString m_filepath; +}; + +bool IniEngine::parse(const QByteArray &data) +{ + m_rawText = QString::fromUtf8(data); + + // Write to temp file for QSettings to parse + QTemporaryFile tmp; + tmp.setAutoRemove(true); + if (!tmp.open()) return false; + tmp.write(data); + tmp.flush(); + + QSettings ini(tmp.fileName(), QSettings::IniFormat); + if (ini.status() != QSettings::NoError) + return false; + + m_root = std::make_unique(QStringLiteral("root")); + + // Get all groups (sections) + QStringList groups = ini.childGroups(); + + // General section (keys not in any group) + QStringList generalKeys = ini.childKeys(); + if (!generalKeys.isEmpty()) { + auto *general = m_root->addChild(QStringLiteral("General")); + general->columnNames = {QStringLiteral("Key"), QStringLiteral("Value")}; + for (const auto &key : generalKeys) { + general->rows.append({QVariant(key), ini.value(key)}); + } + } + + // Named sections + for (const auto &group : groups) { + auto *section = m_root->addChild(group); + section->columnNames = {QStringLiteral("Key"), QStringLiteral("Value")}; + + ini.beginGroup(group); + QStringList keys = ini.childKeys(); + for (const auto &key : keys) { + section->rows.append({QVariant(key), ini.value(key)}); + } + ini.endGroup(); + } + + return !m_root->children.isEmpty(); +} + +QByteArray IniEngine::serialize() const +{ + if (!m_root) return {}; + + QByteArray result; + for (const auto *section : m_root->children) { + if (section->name != QStringLiteral("General")) { + result.append('['); + result.append(section->name.toUtf8()); + result.append("]\n"); + } + for (const auto &row : section->rows) { + result.append(row[0].toString().toUtf8()); + result.append('='); + result.append(row[1].toString().toUtf8()); + result.append('\n'); + } + result.append('\n'); + } + return result; +} + +QByteArray IniEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!node) return {}; + QByteArray result; + // If it's a section node (General or named) + if (node->parent && node->parent->name == QStringLiteral("root")) { + if (node->name != QStringLiteral("General")) { + result.append('['); + result.append(node->name.toUtf8()); + result.append("]\n"); + } + for (const auto &row : node->rows) { + result.append(row[0].toString().toUtf8()); + result.append('='); + result.append(row[1].toString().toUtf8()); + result.append('\n'); + } + } else { + return serialize(); + } + return result; +} + +// Factory helpers — called by TextFormatEngine::createForFile() +std::unique_ptr createJsonEngine(); +std::unique_ptr createXmlEngine(); +std::unique_ptr createCborEngine(); +std::unique_ptr createYamlEngine(); +std::unique_ptr createTomlEngine(); + +std::unique_ptr createIniEngine() +{ + return std::make_unique(); +} + +std::unique_ptr TextFormatEngine::createForFile(const QString &filepath) +{ + QFileInfo fi(filepath); + QString ext = fi.suffix().toLower(); + + if (ext == QStringLiteral("json")) + return createJsonEngine(); + if (ext == QStringLiteral("xml") || ext == QStringLiteral("svg") + || ext == QStringLiteral("xhtml") || ext == QStringLiteral("plist")) + return createXmlEngine(); + if (ext == QStringLiteral("cbor")) + return createCborEngine(); + if (ext == QStringLiteral("ini") || ext == QStringLiteral("cfg") + || ext == QStringLiteral("conf") || ext == QStringLiteral("desktop") + || ext == QStringLiteral("inf")) + return createIniEngine(); + if (ext == QStringLiteral("yaml") || ext == QStringLiteral("yml")) + return createYamlEngine(); + if (ext == QStringLiteral("toml")) + return createTomlEngine(); + + return nullptr; +} diff --git a/wlx/structview/src/JsonEngine.cpp b/wlx/structview/src/JsonEngine.cpp new file mode 100644 index 0000000..587847e --- /dev/null +++ b/wlx/structview/src/JsonEngine.cpp @@ -0,0 +1,378 @@ +#include "TextFormatEngine.h" + +#include +#include +#include +#include + +/// JSON engine: builds a document tree from JSON. +/// +/// - Objects → child nodes for nested objects/arrays, grid rows for scalars +/// - Arrays of objects with shared keys → tabular grid on parent +/// - Arrays of primitives → 1-column Value grid +class JsonEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_root.get(); } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("JSON"); } + + /// Also used by CborEngine + bool parseFromJson(const QJsonValue &root, const QString &rootName); + +private: + void buildTree(DocumentNode *node, const QJsonValue &value); + QJsonValue treeToJson(const DocumentNode *node) const; + + /// Check if a JSON array is "tabular" (array of objects with shared keys) + static bool isTabularArray(const QJsonArray &arr, QStringList &columns); + + /// Convert a QJsonValue to a display string + static QString valueToString(const QJsonValue &v); + + std::unique_ptr m_root; + QString m_rawText; +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +QString JsonEngine::valueToString(const QJsonValue &v) +{ + switch (v.type()) { + case QJsonValue::String: return v.toString(); + case QJsonValue::Double: return QString::number(v.toDouble(), 'g', 15); + case QJsonValue::Bool: return v.toBool() ? QStringLiteral("true") : QStringLiteral("false"); + case QJsonValue::Null: return QStringLiteral("null"); + case QJsonValue::Object: + return QString::fromUtf8(QJsonDocument(v.toObject()).toJson(QJsonDocument::Compact)); + case QJsonValue::Array: + return QString::fromUtf8(QJsonDocument(v.toArray()).toJson(QJsonDocument::Compact)); + default: return QString(); + } +} + +bool JsonEngine::isTabularArray(const QJsonArray &arr, QStringList &columns) +{ + if (arr.isEmpty()) return false; + + // All elements must be objects + for (const auto &elem : arr) { + if (!elem.isObject()) return false; + } + + // Collect union of keys preserving first-seen order + QSet seen; + for (const auto &elem : arr) { + QJsonObject obj = elem.toObject(); + for (auto it = obj.begin(); it != obj.end(); ++it) { + if (!seen.contains(it.key())) { + seen.insert(it.key()); + columns.append(it.key()); + } + } + } + + return columns.size() >= 1; +} + +// --------------------------------------------------------------------------- +// Tree building +// --------------------------------------------------------------------------- + +void JsonEngine::buildTree(DocumentNode *node, const QJsonValue &value) +{ + if (value.isObject()) { + QJsonObject obj = value.toObject(); + + // Separate scalars (grid rows) from containers (child nodes) + QStringList scalarKeys; + QList> containers; + + for (auto it = obj.begin(); it != obj.end(); ++it) { + if (it.value().isObject() || it.value().isArray()) { + containers.append({it.key(), it.value()}); + } else { + scalarKeys.append(it.key()); + } + } + + // Scalars → 2-column grid: Key | Value + if (!scalarKeys.isEmpty()) { + node->columnNames = {QStringLiteral("Key"), QStringLiteral("Value")}; + for (const auto &key : scalarKeys) { + node->rows.append({QVariant(key), QVariant(valueToString(obj[key]))}); + } + } + + // Containers → child nodes + for (const auto &[key, val] : containers) { + auto *child = node->addChild(key); + buildTree(child, val); + } + + } else if (value.isArray()) { + QJsonArray arr = value.toArray(); + QStringList tabularCols; + + if (isTabularArray(arr, tabularCols)) { + // Tabular: display as multi-column grid + node->columnNames = tabularCols; + for (const auto &elem : arr) { + QJsonObject obj = elem.toObject(); + QVector row; + row.reserve(tabularCols.size()); + for (const auto &col : tabularCols) { + row.append(QVariant(valueToString(obj[col]))); + } + node->rows.append(std::move(row)); + } + + // But also check for nested containers in each object + for (int i = 0; i < arr.size(); ++i) { + QJsonObject obj = arr[i].toObject(); + bool hasContainers = false; + for (auto it = obj.begin(); it != obj.end(); ++it) { + if (it.value().isObject() || it.value().isArray()) { + hasContainers = true; + break; + } + } + if (hasContainers) { + auto *child = node->addChild(QStringLiteral("[%1]").arg(i)); + buildTree(child, arr[i]); + } + } + } else { + // Non-tabular: individual items + if (arr.isEmpty()) return; + + // Check if all elements are primitives + bool allPrimitive = true; + for (const auto &elem : arr) { + if (elem.isObject() || elem.isArray()) { + allPrimitive = false; + break; + } + } + + if (allPrimitive) { + // Value grid + node->columnNames = {QStringLiteral("Value")}; + for (int i = 0; i < arr.size(); ++i) { + node->rows.append({QVariant(valueToString(arr[i]))}); + } + } else { + // Mixed: child nodes for each element + for (int i = 0; i < arr.size(); ++i) { + auto *child = node->addChild(QStringLiteral("[%1]").arg(i)); + buildTree(child, arr[i]); + } + } + } + } + // Scalars at root level are handled by the parent +} + +// --------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------- + +bool JsonEngine::parse(const QByteArray &data) +{ + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) + return false; + + m_rawText = QString::fromUtf8(doc.toJson(QJsonDocument::Indented)); + + QJsonValue root; + QString rootName = QStringLiteral("root"); + if (doc.isObject()) { + root = doc.object(); + } else if (doc.isArray()) { + root = doc.array(); + } else { + return false; + } + + return parseFromJson(root, rootName); +} + +bool JsonEngine::parseFromJson(const QJsonValue &root, const QString &rootName) +{ + m_root = std::make_unique(rootName); + buildTree(m_root.get(), root); + return true; +} + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +QJsonValue JsonEngine::treeToJson(const DocumentNode *node) const +{ + // If node has child nodes, it's a container + if (!node->children.isEmpty()) { + // Check if children are array-indexed + bool isArray = !node->children.isEmpty() + && node->children[0]->name.startsWith('['); + + if (isArray) { + QJsonArray arr; + for (const auto *child : node->children) { + arr.append(treeToJson(child)); + } + return arr; + } else { + QJsonObject obj; + // First add grid rows (Key | Value scalars) + if (node->columnNames.size() == 2 + && node->columnNames[0] == QStringLiteral("Key")) { + for (const auto &row : node->rows) { + QString key = row[0].toString(); + QString val = row[1].toString(); + // Try to preserve types + if (val == QStringLiteral("null")) + obj[key] = QJsonValue::Null; + else if (val == QStringLiteral("true")) + obj[key] = true; + else if (val == QStringLiteral("false")) + obj[key] = false; + else { + bool ok; + double num = val.toDouble(&ok); + if (ok) obj[key] = num; + else obj[key] = val; + } + } + } + // Then add child containers + for (const auto *child : node->children) { + obj[child->name] = treeToJson(child); + } + return obj; + } + } + + // Leaf node with tabular grid → array of objects + bool isTabular = false; + if (node->columnNames.size() > 2) { + isTabular = true; + } else if (node->columnNames.size() == 2 + && node->columnNames[0] != QStringLiteral("Key") + && node->columnNames[0] != QStringLiteral("Index")) { + isTabular = true; + } else if (node->columnNames.size() == 1 + && node->columnNames[0] != QStringLiteral("Value")) { + isTabular = true; + } + + if (isTabular) { + QJsonArray arr; + for (const auto &row : node->rows) { + QJsonObject obj; + for (int c = 0; c < node->columnNames.size() && c < row.size(); ++c) { + QString val = row[c].toString(); + if (val == QStringLiteral("null")) + obj[node->columnNames[c]] = QJsonValue::Null; + else if (val == QStringLiteral("true")) + obj[node->columnNames[c]] = true; + else if (val == QStringLiteral("false")) + obj[node->columnNames[c]] = false; + else { + bool ok; + double num = val.toDouble(&ok); + if (ok) obj[node->columnNames[c]] = num; + else obj[node->columnNames[c]] = val; + } + } + arr.append(obj); + } + return arr; + } + + // Key | Value → object + if (node->columnNames.size() == 2 + && node->columnNames[0] == QStringLiteral("Key")) { + QJsonObject obj; + for (const auto &row : node->rows) { + QString key = row[0].toString(); + QString val = row[1].toString(); + if (val == QStringLiteral("null")) + obj[key] = QJsonValue::Null; + else if (val == QStringLiteral("true")) + obj[key] = true; + else if (val == QStringLiteral("false")) + obj[key] = false; + else { + bool ok; + double num = val.toDouble(&ok); + if (ok) obj[key] = num; + else obj[key] = val; + } + } + return obj; + } + + // Value only → array + if (node->columnNames.size() == 1 + && node->columnNames[0] == QStringLiteral("Value")) { + QJsonArray arr; + for (const auto &row : node->rows) { + QString val = row[0].toString(); + if (val == QStringLiteral("null")) + arr.append(QJsonValue::Null); + else if (val == QStringLiteral("true")) + arr.append(true); + else if (val == QStringLiteral("false")) + arr.append(false); + else { + bool ok; + double num = val.toDouble(&ok); + if (ok) arr.append(num); + else arr.append(val); + } + } + return arr; + } + + return QJsonValue(); +} + +QByteArray JsonEngine::serialize() const +{ + if (!m_root) return {}; + + QJsonValue val = treeToJson(m_root.get()); + QJsonDocument doc; + if (val.isObject()) + doc = QJsonDocument(val.toObject()); + else if (val.isArray()) + doc = QJsonDocument(val.toArray()); + + return doc.toJson(QJsonDocument::Indented); +} + +QByteArray JsonEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!node) return {}; + QJsonValue val = treeToJson(node); + if (val.isObject()) { + return QJsonDocument(val.toObject()).toJson(QJsonDocument::Indented); + } else if (val.isArray()) { + return QJsonDocument(val.toArray()).toJson(QJsonDocument::Indented); + } else { + return valueToString(val).toUtf8(); + } +} + +// Factory helper +std::unique_ptr createJsonEngine() +{ + return std::make_unique(); +} diff --git a/wlx/structview/src/StructViewWidget.cpp b/wlx/structview/src/StructViewWidget.cpp new file mode 100644 index 0000000..1343e31 --- /dev/null +++ b/wlx/structview/src/StructViewWidget.cpp @@ -0,0 +1,655 @@ +#include "StructViewWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QtWlPlugin; + +StructViewWidget::StructViewWidget(QWidget *parent) + : QWidget(parent) + , m_fm(nullptr) +{ + setupUi(); + + // Apply saved theme + ThemeManager::applyTheme(this, ThemeManager::currentTheme()); +} + +StructViewWidget::~StructViewWidget() +{ + // Neutralize FocusManager FIRST — it has a global event filter on qApp + // and a connection to qApp::focusChanged. If these fire during child + // destruction (when focus shifts as widgets die), they access + // half-destroyed objects and crash. + if (m_fm) { + if (qApp) { + qApp->removeEventFilter(m_fm); + disconnect(qApp, nullptr, m_fm, nullptr); + } + } + + // Block signals so that Qt's arbitrary child destruction order cannot + // trigger dataChanged/selectionChanged callbacks on dead objects. + if (m_gridView) + m_gridView->blockSignals(true); + if (m_gridModel) + m_gridModel->blockSignals(true); + if (m_filterProxy) + m_filterProxy->blockSignals(true); + if (m_grid && m_grid->undoStack()) + m_grid->undoStack()->blockSignals(true); +} + +void StructViewWidget::setupUi() +{ + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // --- Create the primary view and FocusManager FIRST --- + // (setupToolbar needs m_fm for shortcut registration) + m_gridView = new QTableView; + m_gridModel = new QStandardItemModel(this); + m_filterProxy = new SequentialRowProxyModel(this); + m_filterProxy->setSourceModel(m_gridModel); + m_filterProxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + + // Install filterable header (must be before setModel) + m_filterHeader = new FilterableHeaderView(Qt::Horizontal, m_gridView); + m_filterHeader->setFilterEnabled(true); + m_filterHeader->setStretchLastSection(true); + m_gridView->setHorizontalHeader(m_filterHeader); + + m_gridView->setModel(m_filterProxy); + m_gridView->setSortingEnabled(true); + m_gridView->setAlternatingRowColors(true); + m_gridView->setSelectionBehavior(QAbstractItemView::SelectItems); + m_gridView->setSelectionMode(QAbstractItemView::ExtendedSelection); + + connect(m_filterHeader, &FilterableHeaderView::filterChanged, this, + [this](int column, const QString &text) { + if (m_filterProxy) { + m_filterProxy->setFilterKeyColumn(column); + m_filterProxy->setFilterFixedString(text); + updateStatusBar(); + } + }); + + m_fm = new FocusManager(this, m_gridView, this); + m_grid = new EditableGridWidget(m_gridView, GridMode::MemoryDocument, m_fm, this); + + m_grid->setExtraContextMenuCallback([this](QMenu *menu, const QModelIndex &idx) { + if (!m_currentNode || !idx.isValid()) return; + + QModelIndex sourceIdx = m_filterProxy->mapToSource(idx); + int r = sourceIdx.row(); + int c = sourceIdx.column(); + + QString pathLabel = (m_engine && m_engine->formatName() == QStringLiteral("XML")) + ? QStringLiteral("Copy XPath") + : QStringLiteral("Copy JSONPath"); + + QAction *actCopyPath = menu->addAction(pathLabel); + connect(actCopyPath, &QAction::triggered, this, [this, r, c]() { + QApplication::clipboard()->setText(getCellPath(m_currentNode, r, c)); + }); + + QAction *actCopyKeyValue = nullptr; + bool isTabular = !m_currentNode->columnNames.isEmpty() && m_currentNode->columnNames.size() > 2; + if (isTabular) { + actCopyKeyValue = menu->addAction(QStringLiteral("Copy Subtree")); + } else { + actCopyKeyValue = menu->addAction(QStringLiteral("Copy Key:Value")); + } + + connect(actCopyKeyValue, &QAction::triggered, this, [this, r]() { + if (m_engine && m_engine->formatName() == QStringLiteral("XML")) { + if (m_currentNode->columnNames.size() == 2 && m_currentNode->columnNames[0] == QStringLiteral("Name")) { + QString name = m_currentNode->rows[r][0].toString(); + QString val = m_currentNode->rows[r][1].toString(); + if (name.startsWith('@')) { + QApplication::clipboard()->setText(QStringLiteral("%1=\"%2\"").arg(name.mid(1)).arg(val)); + } else { + QApplication::clipboard()->setText(QStringLiteral("<%1>%2").arg(name).arg(val)); + } + } else if (!m_currentNode->columnNames.isEmpty()) { + QString rowTagName = QStringLiteral("item"); + for (const auto *child : m_currentNode->children) { + if (child->name.contains('[')) { + rowTagName = child->name.section('[', 0, 0); + break; + } + } + DocumentNode temp(rowTagName, m_currentNode->parent); + temp.columnNames = m_currentNode->columnNames; + temp.rows.append(m_currentNode->rows[r]); + QByteArray data = m_engine->serializeSubtree(&temp); + QApplication::clipboard()->setText(QString::fromUtf8(data).trimmed()); + } + } else { + if (m_currentNode->columnNames.size() == 2 && m_currentNode->columnNames[0] == QStringLiteral("Key")) { + QString key = m_currentNode->rows[r][0].toString(); + QString val = m_currentNode->rows[r][1].toString(); + bool ok; + val.toDouble(&ok); + if (val == QStringLiteral("null") || val == QStringLiteral("true") || val == QStringLiteral("false") || ok) { + QApplication::clipboard()->setText(QStringLiteral("\"%1\": %2").arg(key).arg(val)); + } else { + QString escapedVal = val; + escapedVal.replace(QStringLiteral("\""), QStringLiteral("\\\"")); + QApplication::clipboard()->setText(QStringLiteral("\"%1\": \"%2\"").arg(key).arg(escapedVal)); + } + } else if (m_currentNode->columnNames.size() == 1 && m_currentNode->columnNames[0] == QStringLiteral("Value")) { + QApplication::clipboard()->setText(m_currentNode->rows[r][0].toString()); + } else if (!m_currentNode->columnNames.isEmpty()) { + DocumentNode temp(m_currentNode->name, m_currentNode->parent); + temp.columnNames = m_currentNode->columnNames; + temp.rows.append(m_currentNode->rows[r]); + QByteArray data = m_engine->serializeSubtree(&temp); + QString text = QString::fromUtf8(data).trimmed(); + if (m_engine->formatName() == QStringLiteral("JSON") || m_engine->formatName() == QStringLiteral("CBOR")) { + if (text.startsWith('[') && text.endsWith(']')) { + text = text.mid(1, text.length() - 2).trimmed(); + } + } + QApplication::clipboard()->setText(text); + } + } + }); + }); + + // --- Toolbar --- + setupToolbar(); + mainLayout->addWidget(m_toolbar); + + // --- Left panel: Tree view --- + m_treeView = new QTreeView; + m_treeModel = new QStandardItemModel(this); + m_treeView->setModel(m_treeModel); + m_treeView->setHeaderHidden(true); + m_treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_treeView->setMinimumWidth(120); + m_treeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_treeView, &QTreeView::customContextMenuRequested, this, + &StructViewWidget::showTreeContextMenu); + + // --- Right panel: Tabs --- + m_tabWidget = new QTabWidget; + + // Grid tab + auto *gridContainer = new QWidget; + auto *gridLayout = new QVBoxLayout(gridContainer); + gridLayout->setContentsMargins(0, 0, 0, 0); + gridLayout->setSpacing(0); + gridLayout->addWidget(m_grid); + + m_tabWidget->addTab(gridContainer, QStringLiteral("Grid")); + + // Text tab (read-only) + m_textView = new QPlainTextEdit; + m_textView->setReadOnly(true); + m_textView->setFont(QFont(QStringLiteral("monospace"), 10)); + m_textView->setLineWrapMode(QPlainTextEdit::NoWrap); + m_tabWidget->addTab(m_textView, QStringLiteral("Text")); + + // --- Split view --- + m_splitView = new PluginSplitView(m_treeView, m_tabWidget, this); + mainLayout->addWidget(m_splitView, 1); + + // --- Find/replace --- + setupFindReplace(); + mainLayout->addWidget(m_findReplace); + + // --- Status bar --- + m_statusBar = new PluginStatusBar(this); + mainLayout->addWidget(m_statusBar); + + // --- Connect tree selection --- + connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged, + this, &StructViewWidget::onTreeNodeSelected); + +} + +void StructViewWidget::setupToolbar() +{ + m_toolbar = new PluginToolBar(m_fm, this); + + m_dirtyIndicator = new QLabel(QStringLiteral("✓"), this); + m_dirtyIndicator->setContentsMargins(4, 0, 4, 0); + m_toolbar->addWidget(m_dirtyIndicator); + + if (m_grid && m_grid->undoStack()) { + connect(m_grid->undoStack(), &QUndoStack::cleanChanged, this, [this](bool clean) { + if (m_dirtyIndicator) { + m_dirtyIndicator->setText(clean ? QStringLiteral("✓") : QStringLiteral("✱")); + } + }); + } + + auto *actSave = m_toolbar->addToolAction( + QStringLiteral("Save"), QKeySequence::Save, 0, QStringLiteral("document-save")); + connect(actSave, &QAction::triggered, this, &StructViewWidget::onSave); + + auto *actWrap = m_toolbar->addToolAction( + QStringLiteral("Word Wrap"), QKeySequence(), 0, QStringLiteral("format-text-direction-ltr")); + actWrap->setCheckable(true); + connect(actWrap, &QAction::toggled, this, [this](bool on) { + m_grid->setWordWrap(on); + }); + + auto *actGrid = m_toolbar->addToolAction( + QStringLiteral("Grid Lines"), QKeySequence(), 0, QStringLiteral("border-all")); + actGrid->setCheckable(true); + actGrid->setChecked(true); + connect(actGrid, &QAction::toggled, this, [this](bool on) { + m_grid->setShowGrid(on); + }); +} + +void StructViewWidget::setupFindReplace() +{ + m_findReplace = new ScopedFindReplacePanel(m_fm, this); + auto *actFind = m_toolbar->addToolAction( + QStringLiteral("Find"), QKeySequence::Find, 0, QStringLiteral("edit-find")); + connect(actFind, &QAction::triggered, m_findReplace, [this]() { + m_findReplace->showPanel(!m_findReplace->isPanelVisible()); + }); + connect(m_findReplace, &ScopedFindReplacePanel::findRequested, this, + &StructViewWidget::onFind); +} + +bool StructViewWidget::loadFile(const QString &filepath) +{ + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) + return false; + + QByteArray data = file.readAll(); + file.close(); + + m_engine = TextFormatEngine::createForFile(filepath); + if (!m_engine) return false; + + if (!m_engine->parse(data)) + return false; + + m_filepath = filepath; + + // Populate tree + populateTree(); + + // Set text tab + m_textView->setPlainText(m_engine->rawText()); + + // Status bar + QFileInfo fi(filepath); + m_statusBar->setFormatInfo(m_engine->formatName()); + m_statusBar->setEncoding(EncodingUtils::detectEncoding(data)); + + // Select root node + if (m_treeModel->rowCount() > 0) { + m_treeView->setCurrentIndex(m_treeModel->index(0, 0)); + m_treeView->expandAll(); + } + + if (m_grid && m_grid->undoStack()) { + m_grid->undoStack()->clear(); + } + + return true; +} + +void StructViewWidget::populateTree() +{ + m_treeModel->clear(); + DocumentNode *root = m_engine->rootNode(); + if (!root) return; + + auto *rootItem = new QStandardItem(root->name); + rootItem->setData(QVariant::fromValue(reinterpret_cast(root)), + Qt::UserRole); + m_treeModel->appendRow(rootItem); + + populateTreeNode(rootItem, root); +} + +void StructViewWidget::populateTreeNode(QStandardItem *parentItem, DocumentNode *node) +{ + for (auto *child : node->children) { + auto *item = new QStandardItem(child->name); + item->setData(QVariant::fromValue(reinterpret_cast(child)), + Qt::UserRole); + item->setEditable(false); + + // Icon: folder for containers, document for leaves + if (child->isContainer()) { + item->setIcon(style()->standardIcon(QStyle::SP_DirIcon)); + } else { + item->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); + } + + parentItem->appendRow(item); + populateTreeNode(item, child); + } +} + +void StructViewWidget::onTreeNodeSelected(const QModelIndex ¤t, + const QModelIndex & /*previous*/) +{ + if (!current.isValid()) return; + + auto ptr = current.data(Qt::UserRole).value(); + auto *node = reinterpret_cast(ptr); + if (!node) return; + + showNodeData(node); +} + +void StructViewWidget::showNodeData(DocumentNode *node) +{ + m_currentNode = node; + + m_gridModel->clear(); + + if (node->columnNames.isEmpty() && node->rows.isEmpty()) { + // No grid data — show message + m_gridModel->setHorizontalHeaderLabels({QStringLiteral("Info")}); + auto *item = new QStandardItem( + QStringLiteral("Select a child node to view data.")); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + m_gridModel->appendRow(item); + } else { + // Set column headers + m_gridModel->setHorizontalHeaderLabels(node->columnNames); + + // Populate rows + for (const auto &row : node->rows) { + QList items; + for (const auto &val : row) { + auto *item = new QStandardItem(val.toString()); + if (!node->editable) + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + items.append(item); + } + m_gridModel->appendRow(items); + } + } + + m_filterHeader->clearFilters(); + + updateStatusBar(); + + // Hide header if virtual (e.g. Key/Value, Name/Value, or Value only) + bool isVirtual = false; + if (node->columnNames.isEmpty()) { + isVirtual = true; + } else if (node->columnNames.size() == 1 && node->columnNames[0] == QStringLiteral("Value")) { + isVirtual = true; + } else if (node->columnNames.size() == 2 && + (node->columnNames[0] == QStringLiteral("Key") || node->columnNames[0] == QStringLiteral("Name")) && + node->columnNames[1] == QStringLiteral("Value")) { + isVirtual = true; + } + + m_gridView->horizontalHeader()->show(); + m_filterHeader->setHeaderVisible(!isVirtual); + + // Resize columns + m_gridView->horizontalHeader()->setStretchLastSection(true); + m_gridView->resizeColumnsToContents(); +} + +void StructViewWidget::updateStatusBar() +{ + int total = m_gridModel->rowCount(); + int filtered = m_filterProxy->rowCount(); + m_statusBar->setRowCount(filtered, total); +} + +bool StructViewWidget::saveFile() +{ + return saveFileAs(m_filepath); +} + +bool StructViewWidget::saveFileAs(const QString &path) +{ + if (!m_engine) return false; + + // Sync current grid data back to the node + if (m_currentNode && !m_currentNode->columnNames.isEmpty()) { + m_currentNode->rows.clear(); + for (int r = 0; r < m_gridModel->rowCount(); ++r) { + QVector row; + for (int c = 0; c < m_gridModel->columnCount(); ++c) { + auto *item = m_gridModel->item(r, c); + row.append(item ? item->data(Qt::DisplayRole) : QVariant()); + } + m_currentNode->rows.append(std::move(row)); + } + } + + QByteArray output = m_engine->serialize(); + if (output.isEmpty()) return false; + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) + return false; + + file.write(output); + file.close(); + + if (m_grid && m_grid->undoStack()) { + m_grid->undoStack()->setClean(); + } + + return true; +} + +void StructViewWidget::onSave() +{ + FocusManager::expectReloadFocus(); + if (!saveFile()) { + QMessageBox::warning(nullptr, QStringLiteral("Save Error"), + QStringLiteral("Could not save file.")); + } +} + +void StructViewWidget::onFind(bool forward) +{ + // Delegate to grid search + if (!m_gridView->model()) return; + + QString searchText = m_findReplace->findText(); + if (searchText.isEmpty()) return; + + QModelIndex start = m_gridView->currentIndex(); + if (!start.isValid()) + start = m_gridView->model()->index(0, 0); + + QAbstractItemModel *model = m_gridView->model(); + int rows = model->rowCount(); + int cols = model->columnCount(); + int startRow = start.row(); + int startCol = start.column(); + + // Search from current position + for (int i = 0; i < rows * cols; ++i) { + int offset = forward ? (i + 1) : (rows * cols - i - 1); + int r = (startRow + offset / cols) % rows; + int c = (startCol + offset % cols) % cols; + QModelIndex idx = model->index(r, c); + QString text = model->data(idx, Qt::DisplayRole).toString(); + if (text.contains(searchText, Qt::CaseInsensitive)) { + m_gridView->setCurrentIndex(idx); + m_gridView->scrollTo(idx); + return; + } + } +} + +FocusManager *StructViewWidget::focusManager() const { return m_fm; } +EditableGridWidget *StructViewWidget::grid() const { return m_grid; } + +QString StructViewWidget::getSelectionAsText(char sep) +{ + return m_grid->getSelectionAsText(sep); +} + +QString StructViewWidget::getJsonPath(const DocumentNode *node) const +{ + if (!node) return QString(); + QString path; + const DocumentNode *curr = node; + while (curr) { + QString name = curr->name; + if (curr->parent) { + if (name.startsWith('[')) { + path = name + path; + } else { + path = "." + name + path; + } + } else { + path = name + path; + } + curr = curr->parent; + } + return path; +} + +QString StructViewWidget::getXmlPath(const DocumentNode *node) const +{ + if (!node) return QString(); + QString path; + const DocumentNode *curr = node; + while (curr) { + QString name = curr->name; + if (name.contains('[')) { + int openBracket = name.indexOf('['); + int closeBracket = name.indexOf(']'); + if (openBracket != -1 && closeBracket != -1) { + QString tag = name.left(openBracket); + int idx = name.mid(openBracket + 1, closeBracket - openBracket - 1).toInt(); + name = QStringLiteral("%1[%2]").arg(tag).arg(idx + 1); + } + } + path = "/" + name + path; + curr = curr->parent; + } + return path; +} + +QString StructViewWidget::getCellPath(const DocumentNode *node, int r, int c) const +{ + if (!node) return QString(); + if (m_engine && m_engine->formatName() == QStringLiteral("XML")) { + QString basePath = getXmlPath(node); + if (node->columnNames.size() == 2 && node->columnNames[0] == QStringLiteral("Name")) { + if (r >= 0 && r < node->rows.size()) { + QString name = node->rows[r][0].toString(); + return basePath + "/" + name; + } + } else if (!node->columnNames.isEmpty()) { + QString tag = "*"; + for (const auto *child : node->children) { + if (child->name.contains('[')) { + tag = child->name.section('[', 0, 0); + break; + } + } + if (c >= 0 && c < node->columnNames.size()) { + QString colName = node->columnNames[c]; + return basePath + QStringLiteral("/%1[%2]/%3").arg(tag).arg(r + 1).arg(colName); + } + } + return basePath; + } else { + QString basePath = getJsonPath(node); + if (node->columnNames.size() == 2 && node->columnNames[0] == QStringLiteral("Key")) { + if (r >= 0 && r < node->rows.size()) { + QString key = node->rows[r][0].toString(); + return basePath + "." + key; + } + } else if (node->columnNames.size() == 1 && node->columnNames[0] == QStringLiteral("Value")) { + return basePath + QStringLiteral("[%1]").arg(r); + } else if (!node->columnNames.isEmpty()) { + if (c >= 0 && c < node->columnNames.size()) { + QString colName = node->columnNames[c]; + return basePath + QStringLiteral("[%1].%2").arg(r).arg(colName); + } + } + return basePath; + } +} + +void StructViewWidget::showTreeContextMenu(const QPoint &pos) +{ + QModelIndex idx = m_treeView->indexAt(pos); + if (!idx.isValid()) return; + + auto ptr = idx.data(Qt::UserRole).value(); + auto *node = reinterpret_cast(ptr); + if (!node) return; + + QMenu menu(this); + + QString pathLabel = (m_engine && m_engine->formatName() == QStringLiteral("XML")) + ? QStringLiteral("Copy XPath") + : QStringLiteral("Copy JSONPath"); + QAction *actCopyPath = menu.addAction(pathLabel); + + bool isComplex = node->isContainer(); + QAction *actCopySubtree = nullptr; + QAction *actCopyKeyValue = nullptr; + + if (isComplex) { + actCopySubtree = menu.addAction(QStringLiteral("Copy Subtree")); + } else { + actCopyKeyValue = menu.addAction(QStringLiteral("Copy Key:Value")); + } + + QAction *actCopyValue = menu.addAction(QStringLiteral("Copy Value")); + + QAction *res = menu.exec(m_treeView->viewport()->mapToGlobal(pos)); + if (!res) return; + + if (res == actCopyPath) { + QString path = (m_engine && m_engine->formatName() == QStringLiteral("XML")) + ? getXmlPath(node) + : getJsonPath(node); + QApplication::clipboard()->setText(path); + } else if (actCopySubtree && res == actCopySubtree) { + if (m_engine) { + QByteArray data = m_engine->serializeSubtree(node); + QApplication::clipboard()->setText(QString::fromUtf8(data).trimmed()); + } + } else if (actCopyKeyValue && res == actCopyKeyValue) { + if (m_engine) { + QByteArray data = m_engine->serializeSubtree(node); + QApplication::clipboard()->setText(QString::fromUtf8(data).trimmed()); + } + } else if (res == actCopyValue) { + if (m_engine) { + QByteArray data = m_engine->serializeSubtree(node); + QApplication::clipboard()->setText(QString::fromUtf8(data).trimmed()); + } + } +} diff --git a/wlx/structview/src/TomlEngine.cpp b/wlx/structview/src/TomlEngine.cpp new file mode 100644 index 0000000..28f7b55 --- /dev/null +++ b/wlx/structview/src/TomlEngine.cpp @@ -0,0 +1,261 @@ +#include "TextFormatEngine.h" + +#define TOML_IMPLEMENTATION +#include + +#include + +/// TOML engine: builds a document tree from TOML tables. +/// +/// - Tables → child nodes +/// - Arrays of tables → repeated children with tabular grid +/// - Values → Key | Value grid rows +class TomlEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_root.get(); } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("TOML"); } + +private: + void buildTree(DocumentNode *node, const toml::table &tbl); + void buildFromArray(DocumentNode *node, const toml::array &arr); + toml::table treeToToml(const DocumentNode *node) const; + + static QString tomlValueToString(const toml::node &val); + + std::unique_ptr m_root; + QString m_rawText; +}; + +QString TomlEngine::tomlValueToString(const toml::node &val) +{ + if (val.is_string()) return QString::fromStdString(*val.value()); + if (val.is_integer()) return QString::number(*val.value()); + if (val.is_floating_point()) return QString::number(*val.value(), 'g', 15); + if (val.is_boolean()) return *val.value() ? QStringLiteral("true") : QStringLiteral("false"); + if (val.is_date()) { + std::ostringstream ss; ss << *val.as_date(); + return QString::fromStdString(ss.str()); + } + if (val.is_time()) { + std::ostringstream ss; ss << *val.as_time(); + return QString::fromStdString(ss.str()); + } + if (val.is_date_time()) { + std::ostringstream ss; ss << *val.as_date_time(); + return QString::fromStdString(ss.str()); + } + return QString(); +} + +void TomlEngine::buildTree(DocumentNode *node, const toml::table &tbl) +{ + QStringList scalarKeys; + QStringList scalarValues; + + for (auto it = tbl.begin(); it != tbl.end(); ++it) { + QString key = QString::fromStdString(std::string(it->first.str())); + const toml::node &val = it->second; + + if (val.is_table()) { + auto *child = node->addChild(key); + buildTree(child, *val.as_table()); + } else if (val.is_array()) { + const toml::array &arr = *val.as_array(); + // Check if it's an array of tables + if (!arr.empty() && arr.front().is_table()) { + auto *child = node->addChild(QStringLiteral("[[%1]]").arg(key)); + buildFromArray(child, arr); + } else { + // Simple array → display as comma-separated string + QStringList items; + for (size_t i = 0; i < arr.size(); ++i) { + const auto &elem = arr[i]; + if (elem.is_string()) + items << QString::fromStdString(*elem.value()); + else if (elem.is_integer()) + items << QString::number(*elem.value()); + else if (elem.is_floating_point()) + items << QString::number(*elem.value(), 'g', 15); + else if (elem.is_boolean()) + items << (*elem.value() ? QStringLiteral("true") : QStringLiteral("false")); + else + items << QStringLiteral("?"); + } + scalarKeys.append(key); + scalarValues.append(QStringLiteral("[%1]").arg(items.join(QStringLiteral(", ")))); + } + } else { + scalarKeys.append(key); + scalarValues.append(tomlValueToString(val)); + } + } + + if (!scalarKeys.isEmpty()) { + node->columnNames = {QStringLiteral("Key"), QStringLiteral("Value")}; + for (int i = 0; i < scalarKeys.size(); ++i) { + node->rows.append({QVariant(scalarKeys[i]), QVariant(scalarValues[i])}); + } + } +} + +void TomlEngine::buildFromArray(DocumentNode *node, const toml::array &arr) +{ + // Array of tables → tabular grid + QStringList columns; + QSet seen; + + // Collect all keys + for (size_t i = 0; i < arr.size(); ++i) { + if (!arr[i].is_table()) continue; + const toml::table &tbl = *arr[i].as_table(); + for (auto it = tbl.begin(); it != tbl.end(); ++it) { + QString key = QString::fromStdString(std::string(it->first.str())); + if (!it->second.is_table() && !it->second.is_array()) { + if (!seen.contains(key)) { + seen.insert(key); + columns.append(key); + } + } + } + } + + if (!columns.isEmpty()) { + node->columnNames = columns; + for (size_t i = 0; i < arr.size(); ++i) { + if (!arr[i].is_table()) continue; + const toml::table &tbl = *arr[i].as_table(); + QVector row; + row.reserve(columns.size()); + for (const auto &col : columns) { + auto val = tbl[col.toStdString()]; + if (val) { + // node_view → get the underlying node by reference + const toml::node &n = *val.node(); + row.append(QVariant(tomlValueToString(n))); + } else { + row.append(QVariant()); + } + } + node->rows.append(std::move(row)); + } + } + + // Also add child nodes for tables that have nested tables + for (size_t i = 0; i < arr.size(); ++i) { + if (!arr[i].is_table()) continue; + const toml::table &tbl = *arr[i].as_table(); + bool hasNested = false; + for (auto it = tbl.begin(); it != tbl.end(); ++it) { + if (it->second.is_table() || it->second.is_array()) { + hasNested = true; + break; + } + } + if (hasNested) { + auto *child = node->addChild(QStringLiteral("[%1]").arg(i)); + buildTree(child, tbl); + } + } +} + +bool TomlEngine::parse(const QByteArray &data) +{ + try { + m_rawText = QString::fromUtf8(data); + + std::string str = data.toStdString(); + toml::table tbl = toml::parse(str); + + m_root = std::make_unique(QStringLiteral("root")); + buildTree(m_root.get(), tbl); + return true; + + } catch (const toml::parse_error &) { + return false; + } +} + +toml::table TomlEngine::treeToToml(const DocumentNode *node) const +{ + toml::table tbl; + + // Grid rows (Key | Value) + if (node->columnNames.size() == 2 + && node->columnNames[0] == QStringLiteral("Key")) { + for (const auto &row : node->rows) { + std::string key = row[0].toString().toStdString(); + std::string val = row[1].toString().toStdString(); + // Try to preserve types + if (val == "true") tbl.insert(key, true); + else if (val == "false") tbl.insert(key, false); + else { + try { + size_t pos; + int64_t ival = std::stoll(val, &pos); + if (pos == val.size()) { tbl.insert(key, ival); continue; } + } catch (...) {} + try { + size_t pos; + double dval = std::stod(val, &pos); + if (pos == val.size()) { tbl.insert(key, dval); continue; } + } catch (...) {} + tbl.insert(key, val); + } + } + } + + // Child nodes + for (const auto *child : node->children) { + std::string childKey = child->name.toStdString(); + // Strip [[ ]] for array of tables + if (child->name.startsWith(QStringLiteral("[["))) { + childKey = child->name.mid(2, child->name.size() - 4).toStdString(); + toml::array arr; + // If child has tabular data, each row is a table + if (!child->columnNames.isEmpty()) { + for (const auto &row : child->rows) { + toml::table entry; + for (int c = 0; c < child->columnNames.size() && c < row.size(); ++c) { + entry.insert(child->columnNames[c].toStdString(), + row[c].toString().toStdString()); + } + arr.push_back(std::move(entry)); + } + } + tbl.insert(childKey, std::move(arr)); + } else { + tbl.insert(childKey, treeToToml(child)); + } + } + + return tbl; +} + +QByteArray TomlEngine::serialize() const +{ + if (!m_root) return {}; + + toml::table tbl = treeToToml(m_root.get()); + + std::ostringstream ss; + ss << toml::toml_formatter(tbl); + return QByteArray::fromStdString(ss.str()); +} + +QByteArray TomlEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!node) return {}; + toml::table tbl = treeToToml(node); + std::ostringstream ss; + ss << toml::toml_formatter(tbl); + return QByteArray::fromStdString(ss.str()); +} + +std::unique_ptr createTomlEngine() +{ + return std::make_unique(); +} diff --git a/wlx/structview/src/XmlEngine.cpp b/wlx/structview/src/XmlEngine.cpp new file mode 100644 index 0000000..69a5628 --- /dev/null +++ b/wlx/structview/src/XmlEngine.cpp @@ -0,0 +1,265 @@ +#include "TextFormatEngine.h" + +#include +#include +#include +#include +#include + +/// XML engine: builds a document tree from the DOM. +/// +/// - Each element → tree node +/// - Repeating children with same tag → tabular grid on parent (attributes as @columns) +/// - Non-repeating children with text → 2-column Name | Value grid +class XmlEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_root.get(); } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("XML"); } + +private: + void buildTree(DocumentNode *node, const QDomElement &elem); + QDomElement treeToXml(QDomDocument &doc, const DocumentNode *node) const; + + std::unique_ptr m_root; + QString m_rawText; + QDomDocument m_originalDoc; +}; + +void XmlEngine::buildTree(DocumentNode *node, const QDomElement &elem) +{ + // Count child element tag frequencies + QMap tagCounts; + QMap> tagElements; + QDomNodeList children = elem.childNodes(); + + for (int i = 0; i < children.count(); ++i) { + QDomNode child = children.at(i); + if (child.isElement()) { + QString tag = child.toElement().tagName(); + tagCounts[tag]++; + tagElements[tag].append(child.toElement()); + } + } + + // Find repeating tags (tabular data) + QString tabularTag; + int maxCount = 0; + for (auto it = tagCounts.begin(); it != tagCounts.end(); ++it) { + if (it.value() > maxCount) { + maxCount = it.value(); + tabularTag = it.key(); + } + } + + if (maxCount > 1) { + // Repeating elements → tabular grid + QStringList columns; + QSet seen; + const auto &elems = tagElements[tabularTag]; + + for (const auto &rowEl : elems) { + // Attributes (prefixed with @) + QDomNamedNodeMap attrs = rowEl.attributes(); + for (int a = 0; a < attrs.count(); ++a) { + QString attrName = QStringLiteral("@") + attrs.item(a).toAttr().name(); + if (!seen.contains(attrName)) { + seen.insert(attrName); + columns.append(attrName); + } + } + // Child element text values + QDomNodeList rowChildren = rowEl.childNodes(); + for (int j = 0; j < rowChildren.count(); ++j) { + if (rowChildren.at(j).isElement()) { + QString tag = rowChildren.at(j).toElement().tagName(); + if (!seen.contains(tag)) { + seen.insert(tag); + columns.append(tag); + } + } + } + } + + node->columnNames = columns; + for (const auto &rowEl : elems) { + QVector row; + row.reserve(columns.size()); + for (const auto &col : columns) { + if (col.startsWith('@')) { + row.append(QVariant(rowEl.attribute(col.mid(1)))); + } else { + QDomElement sub = rowEl.firstChildElement(col); + row.append(QVariant(sub.isNull() ? QString() : sub.text())); + } + } + node->rows.append(std::move(row)); + } + + // Also create child nodes for each repeating element (for deep navigation) + for (int i = 0; i < elems.size(); ++i) { + const auto &rowEl = elems[i]; + // Check if element has nested elements (not just text children) + bool hasNestedElements = false; + QDomNodeList rowChildren = rowEl.childNodes(); + for (int j = 0; j < rowChildren.count(); ++j) { + if (rowChildren.at(j).isElement()) { + QDomElement childEl = rowChildren.at(j).toElement(); + // Check if the child itself has children (deep nesting) + if (childEl.hasChildNodes() && !childEl.firstChildElement().isNull()) { + hasNestedElements = true; + break; + } + } + } + if (hasNestedElements) { + auto *child = node->addChild( + QStringLiteral("%1[%2]").arg(tabularTag).arg(i)); + buildTree(child, rowEl); + } + } + + // Non-repeating elements → also add as child nodes + for (auto it = tagElements.begin(); it != tagElements.end(); ++it) { + if (it.key() == tabularTag) continue; + for (const auto &el : it.value()) { + auto *child = node->addChild(el.tagName()); + buildTree(child, el); + } + } + } else { + // No repeating elements → either leaf text or mixed content + // Collect all child elements as Key | Value pairs or child nodes + QStringList scalarNames; + QStringList scalarValues; + + for (int i = 0; i < children.count(); ++i) { + QDomNode child = children.at(i); + if (!child.isElement()) continue; + + QDomElement childEl = child.toElement(); + + // If element has child elements itself, make it a tree node + if (!childEl.firstChildElement().isNull()) { + auto *childNode = node->addChild(childEl.tagName()); + buildTree(childNode, childEl); + } else { + // Leaf element → grid row + scalarNames.append(childEl.tagName()); + scalarValues.append(childEl.text()); + } + } + + // Add attributes to grid too + QDomNamedNodeMap attrs = elem.attributes(); + for (int a = 0; a < attrs.count(); ++a) { + scalarNames.prepend(QStringLiteral("@") + attrs.item(a).toAttr().name()); + scalarValues.prepend(attrs.item(a).toAttr().value()); + } + + if (!scalarNames.isEmpty()) { + node->columnNames = {QStringLiteral("Name"), QStringLiteral("Value")}; + for (int i = 0; i < scalarNames.size(); ++i) { + node->rows.append({QVariant(scalarNames[i]), QVariant(scalarValues[i])}); + } + } + } +} + +bool XmlEngine::parse(const QByteArray &data) +{ + QString errorMsg; + int errorLine, errorCol; + if (!m_originalDoc.setContent(data, &errorMsg, &errorLine, &errorCol)) + return false; + + m_rawText = m_originalDoc.toString(2); + + QDomElement rootEl = m_originalDoc.documentElement(); + if (rootEl.isNull()) return false; + + m_root = std::make_unique(rootEl.tagName()); + buildTree(m_root.get(), rootEl); + + return true; +} + +QDomElement XmlEngine::treeToXml(QDomDocument &doc, const DocumentNode *node) const +{ + QDomElement elem = doc.createElement(node->name.contains('[') + ? node->name.section('[', 0, 0) : node->name); + + // Grid data: Name | Value → child elements or attributes + if (node->columnNames.size() == 2 + && node->columnNames[0] == QStringLiteral("Name")) { + for (const auto &row : node->rows) { + QString name = row[0].toString(); + QString value = row[1].toString(); + if (name.startsWith('@')) { + elem.setAttribute(name.mid(1), value); + } else { + QDomElement child = doc.createElement(name); + child.appendChild(doc.createTextNode(value)); + elem.appendChild(child); + } + } + } + // Tabular grid → repeating child elements + else if (!node->columnNames.isEmpty()) { + // Determine the tag name from the first column or node context + QString rowTag = node->name; + for (const auto &row : node->rows) { + QDomElement rowElem = doc.createElement(rowTag); + for (int c = 0; c < node->columnNames.size() && c < row.size(); ++c) { + QString col = node->columnNames[c]; + QString val = row[c].toString(); + if (col.startsWith('@')) { + if (!val.isEmpty()) + rowElem.setAttribute(col.mid(1), val); + } else { + QDomElement child = doc.createElement(col); + child.appendChild(doc.createTextNode(val)); + rowElem.appendChild(child); + } + } + elem.appendChild(rowElem); + } + } + + // Child nodes + for (const auto *child : node->children) { + elem.appendChild(treeToXml(doc, child)); + } + + return elem; +} + +QByteArray XmlEngine::serialize() const +{ + if (!m_root) return {}; + + QDomDocument doc; + QDomProcessingInstruction pi = doc.createProcessingInstruction( + QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")); + doc.appendChild(pi); + doc.appendChild(treeToXml(doc, m_root.get())); + + return doc.toByteArray(2); +} + +QByteArray XmlEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!node) return {}; + QDomDocument doc; + QDomElement elem = treeToXml(doc, node); + doc.appendChild(elem); + return doc.toByteArray(2); +} + +std::unique_ptr createXmlEngine() +{ + return std::make_unique(); +} diff --git a/wlx/structview/src/YamlEngine.cpp b/wlx/structview/src/YamlEngine.cpp new file mode 100644 index 0000000..eadc005 --- /dev/null +++ b/wlx/structview/src/YamlEngine.cpp @@ -0,0 +1,239 @@ +#include "TextFormatEngine.h" + +#include +#include + +/// YAML engine: builds a document tree from YAML mappings/sequences. +/// +/// - Mapping → child nodes for nested mappings, grid rows for scalars +/// - Sequence → numbered children [0], [1], ... +/// - Scalar → leaf value in parent's grid +class YamlEngine : public TextFormatEngine { +public: + bool parse(const QByteArray &data) override; + DocumentNode *rootNode() const override { return m_root.get(); } + QByteArray serialize() const override; + QByteArray serializeSubtree(const DocumentNode *node) const override; + QString rawText() const override { return m_rawText; } + QString formatName() const override { return QStringLiteral("YAML"); } + +private: + void buildTree(DocumentNode *node, const YAML::Node &yamlNode); + YAML::Node treeToYaml(const DocumentNode *node) const; + + std::unique_ptr m_root; + QString m_rawText; +}; + +void YamlEngine::buildTree(DocumentNode *node, const YAML::Node &yamlNode) +{ + if (yamlNode.IsMap()) { + QStringList scalarKeys; + QStringList scalarValues; + QList> containers; + + for (auto it = yamlNode.begin(); it != yamlNode.end(); ++it) { + QString key = QString::fromStdString(it->first.as()); + const YAML::Node &val = it->second; + + if (val.IsMap() || val.IsSequence()) { + containers.append({key, val}); + } else { + scalarKeys.append(key); + scalarValues.append( + val.IsNull() ? QStringLiteral("null") + : QString::fromStdString(val.as())); + } + } + + if (!scalarKeys.isEmpty()) { + node->columnNames = {QStringLiteral("Key"), QStringLiteral("Value")}; + for (int i = 0; i < scalarKeys.size(); ++i) { + node->rows.append({QVariant(scalarKeys[i]), QVariant(scalarValues[i])}); + } + } + + for (const auto &[key, val] : containers) { + auto *child = node->addChild(key); + buildTree(child, val); + } + + } else if (yamlNode.IsSequence()) { + // Check if all elements are maps with shared keys (tabular) + bool allMaps = true; + QStringList columns; + QSet seen; + + for (size_t i = 0; i < yamlNode.size(); ++i) { + if (!yamlNode[i].IsMap()) { + allMaps = false; + break; + } + for (auto it = yamlNode[i].begin(); it != yamlNode[i].end(); ++it) { + QString key = QString::fromStdString(it->first.as()); + if (!seen.contains(key)) { + seen.insert(key); + columns.append(key); + } + } + } + + if (allMaps && !columns.isEmpty()) { + // Tabular grid + node->columnNames = columns; + for (size_t i = 0; i < yamlNode.size(); ++i) { + QVector row; + row.reserve(columns.size()); + for (const auto &col : columns) { + auto val = yamlNode[i][col.toStdString()]; + if (val && val.IsScalar()) + row.append(QVariant(QString::fromStdString(val.as()))); + else if (val && val.IsNull()) + row.append(QVariant(QStringLiteral("null"))); + else + row.append(QVariant()); + } + node->rows.append(std::move(row)); + } + } else { + // Check if all scalars + bool allScalar = true; + for (size_t i = 0; i < yamlNode.size(); ++i) { + if (!yamlNode[i].IsScalar() && !yamlNode[i].IsNull()) { + allScalar = false; + break; + } + } + + if (allScalar) { + node->columnNames = {QStringLiteral("Value")}; + for (size_t i = 0; i < yamlNode.size(); ++i) { + QString val = yamlNode[i].IsNull() + ? QStringLiteral("null") + : QString::fromStdString(yamlNode[i].as()); + node->rows.append({QVariant(val)}); + } + } else { + // Mixed: numbered children + for (size_t i = 0; i < yamlNode.size(); ++i) { + auto *child = node->addChild(QStringLiteral("[%1]").arg(i)); + buildTree(child, yamlNode[i]); + } + } + } + } +} + +bool YamlEngine::parse(const QByteArray &data) +{ + try { + fprintf(stderr, "[structview/yaml] parse() called, data size=%d\n", data.size()); + m_rawText = QString::fromUtf8(data); + + YAML::Node yamlRoot = YAML::Load(data.toStdString()); + fprintf(stderr, "[structview/yaml] YAML::Load OK, type=%d IsNull=%d\n", + static_cast(yamlRoot.Type()), yamlRoot.IsNull()); + if (yamlRoot.IsNull()) return false; + + m_root = std::make_unique(QStringLiteral("root")); + buildTree(m_root.get(), yamlRoot); + fprintf(stderr, "[structview/yaml] parse() succeeded\n"); + return true; + + } catch (const YAML::Exception &e) { + fprintf(stderr, "[structview/yaml] YAML::Exception: %s\n", e.what()); + return false; + } catch (const std::exception &e) { + fprintf(stderr, "[structview/yaml] std::exception: %s\n", e.what()); + return false; + } catch (...) { + fprintf(stderr, "[structview/yaml] unknown exception\n"); + return false; + } +} + +YAML::Node YamlEngine::treeToYaml(const DocumentNode *node) const +{ + // Key | Value grid → map + if (node->columnNames.size() == 2 + && node->columnNames[0] == QStringLiteral("Key")) { + YAML::Node map(YAML::NodeType::Map); + for (const auto &row : node->rows) { + std::string key = row[0].toString().toStdString(); + std::string val = row[1].toString().toStdString(); + map[key] = val; + } + // Add child containers + for (const auto *child : node->children) { + map[child->name.toStdString()] = treeToYaml(child); + } + return map; + } + + // Value only → sequence + if (node->columnNames.size() == 1 + && node->columnNames[0] == QStringLiteral("Value")) { + YAML::Node seq(YAML::NodeType::Sequence); + for (const auto &row : node->rows) { + seq.push_back(row[0].toString().toStdString()); + } + return seq; + } + + // Tabular → sequence of maps + if (!node->columnNames.isEmpty()) { + YAML::Node seq(YAML::NodeType::Sequence); + for (const auto &row : node->rows) { + YAML::Node map(YAML::NodeType::Map); + for (int c = 0; c < node->columnNames.size() && c < row.size(); ++c) { + map[node->columnNames[c].toStdString()] = row[c].toString().toStdString(); + } + seq.push_back(map); + } + return seq; + } + + // Container with children only + if (!node->children.isEmpty()) { + bool isArray = node->children[0]->name.startsWith('['); + if (isArray) { + YAML::Node seq(YAML::NodeType::Sequence); + for (const auto *child : node->children) + seq.push_back(treeToYaml(child)); + return seq; + } else { + YAML::Node map(YAML::NodeType::Map); + for (const auto *child : node->children) + map[child->name.toStdString()] = treeToYaml(child); + return map; + } + } + + return YAML::Node(); +} + +QByteArray YamlEngine::serialize() const +{ + if (!m_root) return {}; + + YAML::Node yamlRoot = treeToYaml(m_root.get()); + + YAML::Emitter out; + out << yamlRoot; + + return QByteArray(out.c_str()); +} + +QByteArray YamlEngine::serializeSubtree(const DocumentNode *node) const +{ + if (!node) return {}; + YAML::Node yamlNode = treeToYaml(node); + YAML::Emitter out; + out << yamlNode; + return QByteArray(out.c_str()); +} + +std::unique_ptr createYamlEngine() +{ + return std::make_unique(); +} diff --git a/wlx/structview/src/wlx_entry.cpp b/wlx/structview/src/wlx_entry.cpp new file mode 100644 index 0000000..8188c6b --- /dev/null +++ b/wlx/structview/src/wlx_entry.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include "wlxplugin.h" +#include "StructViewWidget.h" + +// --------------------------------------------------------------------------- +// WLX Plugin Entry Points — all wrapped with WLX_TRY/WLX_CATCH +// --------------------------------------------------------------------------- + +HWND DCPCALL ListLoad(HWND ParentWin, char* FileToLoad, int ShowFlags) +{ + WLX_TRY { + Q_UNUSED(ShowFlags); + fprintf(stderr, "[structview] ListLoad called: %s\n", FileToLoad); + fflush(stderr); + + if (!QApplication::instance()) + return nullptr; + + auto *widget = new StructViewWidget(reinterpret_cast(ParentWin)); + fprintf(stderr, "[structview] Widget created, calling loadFile\n"); + fflush(stderr); + + if (!widget->loadFile(QString::fromUtf8(FileToLoad))) { + fprintf(stderr, "[structview] loadFile FAILED for: %s\n", FileToLoad); + fflush(stderr); + delete widget; + return nullptr; + } + + fprintf(stderr, "[structview] loadFile OK, showing widget\n"); + fflush(stderr); + widget->show(); + return reinterpret_cast(widget); + } WLX_CATCH("ListLoad"); + return nullptr; +} + +void DCPCALL ListCloseWindow(HWND ListWin) +{ + WLX_TRY { + fprintf(stderr, "[structview] ListCloseWindow\n"); + fflush(stderr); + auto *widget = reinterpret_cast(ListWin); + delete widget; + } WLX_CATCH("ListCloseWindow"); +} + +int DCPCALL ListSendCommand(HWND ListWin, int Command, int Parameter) +{ + WLX_TRY { + auto *widget = reinterpret_cast(ListWin); + + switch (Command) { + case lc_copy: { + QString text = widget->getSelectionAsText('\t'); + if (text.isEmpty()) return LISTPLUGIN_ERROR; + QApplication::clipboard()->setText(text); + break; + } + case lc_selectall: + if (widget->grid() && widget->grid()->view()) + widget->grid()->view()->selectAll(); + break; + case lc_focus: + if (Parameter) { + widget->focusManager()->setActive(true); + if (widget->grid() && widget->grid()->view()) + widget->grid()->view()->setFocus(Qt::OtherFocusReason); + } else { + widget->focusManager()->setActive(false); + if (QWidget *fw = QApplication::focusWidget()) { + if (fw == widget || widget->isAncestorOf(fw)) + fw->clearFocus(); + } + } + break; + default: + return LISTPLUGIN_ERROR; + } + return LISTPLUGIN_OK; + } WLX_CATCH("ListSendCommand"); + return LISTPLUGIN_ERROR; +} + +int DCPCALL ListSearchText(HWND ListWin, char* SearchString, int SearchParameter) +{ + WLX_TRY { + auto *widget = reinterpret_cast(ListWin); + if (!widget->grid() || !widget->grid()->view() || !widget->grid()->view()->model()) + return LISTPLUGIN_ERROR; + + QAbstractItemModel *model = widget->grid()->view()->model(); + QString needle = QString::fromUtf8(SearchString); + QTableView *view = widget->grid()->view(); + + // Track search state + QString prev = view->property("needle").value(); + view->setProperty("needle", needle); + + QModelIndex current = view->currentIndex(); + int startRow = 0, startCol = 0; + if (current.isValid()) { + startRow = current.row(); + startCol = current.column(); + } + + bool first = (needle != prev) || (SearchParameter & lcs_findfirst); + bool backward = (SearchParameter & lcs_backwards); + + if (!first) { + if (backward) { + startCol--; + if (startCol < 0) { startCol = model->columnCount() - 1; startRow--; } + } else { + startCol++; + if (startCol >= model->columnCount()) { startCol = 0; startRow++; } + } + } + + int rows = model->rowCount(); + int cols = model->columnCount(); + int total = rows * cols; + + for (int i = 0; i < total; ++i) { + int offset = backward ? -i : i; + int linearStart = startRow * cols + startCol; + int linearIdx = (linearStart + offset + total) % total; + int r = linearIdx / cols; + int c = linearIdx % cols; + + QModelIndex idx = model->index(r, c); + QString cellText = model->data(idx, Qt::DisplayRole).toString(); + + bool match = (SearchParameter & lcs_matchcase) + ? cellText.contains(needle, Qt::CaseSensitive) + : cellText.contains(needle, Qt::CaseInsensitive); + + if (match) { + view->setCurrentIndex(idx); + view->scrollTo(idx); + return LISTPLUGIN_OK; + } + } + + return LISTPLUGIN_ERROR; + } WLX_CATCH("ListSearchText"); + return LISTPLUGIN_ERROR; +} + +void DCPCALL ListGetDetectString(char* DetectString, int maxlen) +{ + snprintf(DetectString, maxlen - 1, + "EXT=\"JSON\" | EXT=\"XML\" | EXT=\"INI\" | EXT=\"CBOR\" | " + "EXT=\"YAML\" | EXT=\"YML\" | EXT=\"TOML\" | " + "EXT=\"DESKTOP\" | EXT=\"INF\""); +} diff --git a/wlx/structview/structview_cbor_edit.png b/wlx/structview/structview_cbor_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..5be5ba2098032690f8be87fbc5d0ef945cda1220 GIT binary patch literal 42598 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 Zwo&84H(86_gBUmFtd5~}_Gz03{{v|3Co%v4 literal 0 HcmV?d00001 diff --git a/wlx/structview/structview_json.png b/wlx/structview/structview_json.png new file mode 100644 index 0000000000000000000000000000000000000000..c84dd183c9bb1584bbe20b1d97b30d8c0d5967fd GIT binary patch literal 75605 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

?c5zej=m)vL59HS7=d`B_F@4KSs12+ zZ|{Co)ls)Q6(YTq^n6y?_LU9E#OKBn@Dc=}!^1!c{peEqVkrL$vsVLzevO0DB!Eld zfnGTDo3?f-F3CzcN`_6z%M)L~5ME9183yX#w=}fWbes^<&;v4t$rL~b2%vtaPrV5N zITjn97>KYwn}plmVV;o+jq@4|I4JPpT0BUU|GS)528-k{AUS4R@D`kq^-N~K>fpsN z!`M6Z)BU(35o(`{Ntg+WERPaaS^O|UM`puWPTidL?@qlk+Q~kU<+u9@cRG_EKnYnw z$}_dEr`yuAU5TH9&urxs=ByAonFoY~e#WQ5bR9Jc^ABCl%PY_XJSUP5Y}y1P0#67$ zv6rBgIZSJ=H?vQu@C2;)8dRVY2t6Z1Unb3rGZl15Y~#YaV`V@ST2bJ^BR!EyiuG~q zun*VZvk(XQ77{REMTxTh0ypd^3KnL>x&ksJ1jp*)-Y>V#n6dAR&&uk!)*4eI!~0E| zTie2hBxi_S8ZWfN)Uqx@6fH}RiHglDhh%5kl=b_X&xnN7yI{QS<4RG}0|pp{wqU&7 zaVbzUM+kQyNcOcqh>frye3)aJa?7YS)f`)D~H&35N zBlbm|gMkbs`=O+jAPvHuszdy;;#V2j%E}ith5VM6CU_A?M@bjs5k-Ypya+Xb1+Bkg8;Bc~HmQ zOzYh0E>$+#y!`yl*?F+lwWHHdRc)HzD@60FB$QN8oi}7eBsv(IC6XePz^iFRb-IUt zeDA1&F&?J{T<{xA>*`87u?rRuJbLv4pfu?WIzr$Lpz#^%eWg0E3_Ki8r;+d$jIb~o zZ>Ahg0^-D$Kra(`%T3mm*iCbs5{ax_(-{P zn?qWGNHT+^j!^nc#&8cdyfY?f4|v85?a#PW%@*}o{gXN#e9|+uuN-m23k)f!0{;=B z+Nk!VEE~I7n%qP!?^57A`d`~190e=3tHigyR(w%Ila8Lv@?C2?+e-rFivEXzR}4SJ(;#zWmmnLl-glvcGzRA zt}KZW#|IP2)S7KDg(em-S5X$No`(Hc*9k+2qpI3NhktDN+HChGP87e9kb5Di8|&Rn!n zRU&gafezDII$<*IIfYKu?#gVb;h1K2hVANA5F}wyui(~{;z}p=nyK%=rJ?So^XLfJ-eg4S#m}+^b`>Br>`8n<%+67$pfbOZsOu ziSa5F^1CI>dA=GK$S*0OYdWo4HUgx0wmSpwmzwNoliTk;EV;Uym|R8nZyN}sFsE+X zSF__X^0!9|>R>uum(LPpl#C#oTs_?5o+xaK?wDU)H7+I+n_N5&kidU4 z@b70XKi7wB%*4K{dob#Xz-W4l#SJ$#RnRS#li>!%8lejNJ2#ZObK-A+Fj>Ka{)T-59@a5+1iGBi0X zniZKF<*{jzP^4?A5RMRXq?{@9hPq#^Q5BV#>j?|E9N{)SWv}u-jvJGe`&}q?x{3x4_w^I{Kh%xo}hJ7J>a`YII7} zi$MZT;)8=1h*CSs{66D+sWy1f)WCoWE?{_UY;XM6FT~s9Ink13_(X>A4A^lUg(FOy zGWon^>ES);tdtVu$NKg1pX*119LTvOykIy}TeP&6f+a%uirf4D^odLz7}ZPjruDIG zYm34VuDWUW?epEj5G6qNh})@-?qM}B)7gOOasCM#GdMCwv?)>sDh!mQe5)adbYUkY7+lpQ7~ zw^92~<;EG6YZh;*47C;%N>q?+=#YWx28#K~t^3&1v&_T>alQP_q!Oxg>4 zDkn(R4^f#8l{(ph1jC0i%*enC*R3`hSj{BEQlIF{Bd-~_A)E)zz#X!hMURuy{H}GC z?`J#d4zPgRqNI{bQB@8N4sOauTqqVRO-7K49IP~g&9=_&o)&qRD2`U40xpBsobHj$ z7n~A`J{{3?ALa-+KiWH+%#yJi!aZoVnxRJDx4-VsA9f4|n;E1g$9CrZJ}+IDc35SN zUVGJnD}C8RZ)D+RZOHM&S8I_v3r}wqCU79iSHqUdWUMF78)_26=dk%KjmH!6dQneO ziTAAFuquHfU3%^Cc7dA8>lAs3bajv~;2w9T&g|neB8|(H@C)hkAlHFovJ5((rulRD ze~92l_c;n`(r+oZAJ;vuKa?)JC-jBm{nzfKOP~6&I48xM@Dsy8ZeN@dalMDr1Iv=f z7MU6~GTl&iU!IbQnFUI`-)*-(1XQRtNKl2Cqg0bUdHfCNs3ps!btLRe$+|xih7XpJ z?;n`h@j0yw2>=OX4y5pWn}CD`qA`m$j~tpLja|<1!NKpz|HPcM)ft;i70np)i+68( zj#|WjJm;-D65?FZj5^$>mL)dKa;IYTksYEUE*UscG?+IldRf^E{nuqpPjz8 zY^)u&=G})RIirb97k5A}UA^}VKwJApXL;xL^_{BynJdWQ$!9Ir+|qNK8C5qbb&&s%u7niJOa>TV zP95HMOw%!PaZX|PAxl2-F>SS#nArEs4CBy3k7P+1{Q@%W9zHucc_)z{tPE5|SIG{4 zN;@N`+ahd`NkETjQ>uj7We9=}CYLAUmI?PxfOERCpU#@Y0hk$JQt)rFnyP}=THE&D zUsYs6BONw}uL=1zCfZ&Kb3OUQqtfg&)!=aj>H-=~whm1X&*w00&b!86V`1Z<vfxPfVt1K*xU zYin%hV3Jr3Xhsv6llb5EKyD2`8QJQcyWgL=tj;V80g4U*bPJxouj1zY5`bpv%G%u{ z!VfD3uQgEV3WDoAb@LQgtpOJXij-IuWE!-+!?z>-~6uT{*Xgw z3Hy*-G>9|tTFag=);#%3)C*MmemEq9T(M0=-+pjtD0gp!R5UDP;9*mkQaJ^b3X)ra z1(JlXH1UjDGJtRsZFXj60>E{J0Xoy}3n(2A4{ueBpS(-=i&6YYk|0uEXs%XUV_e+{ z-&bk+(i-On!X#b;8m^yEB*>M8&h8fM0{j_R;>jN(mGlc)jZufjz8joNouo!`3y!s~ zIRMlWRYh6yfS8G45)YA4dQEoNa$}>cUq|mAZ+Hj>kC7t!w{@n1tG=PJUN)npXLsi_ zOvY4-Eatx)HsefBR>vEUPhM?0I5E%V>^Vd95bB~M7k;q&Z(#FV4==Bhgls<9CAqx& zXOU11cbPmydvVY%DJq#mW7QZwCd?4H`mK#PS}*Yy3C8)AE0AT}A1FRyj#B=Zi^oJ4m zeOOr768dxj5B?WGe9Y7e*wEKF-woIy1Zd53puxz1-!+um*|;`28ooT>3A zza&GG=0dL9?ayP;F3YR; zVP*kfr|^VLi4)w(%ekXQZ|1!FT3rIG+G3t^NzX`~Y+^#dSxX4<<~BxcseH1@Ev)Ue zVGY{BVnbnV;7oTLW-@3)mn#+!B(NvUO*S}WGTwu~KRC|Fa>a)6O$Hs?dVx;Se5H_d~oQQ^KOaab2$yc~?XGatL&o zMA&XdYt9#7o%hlxSv{7Nmm02c&3PP?q$@HJ;3A+z97Z?DXe@uSI2+ zTN*!TkBxdZbbyv!G?6$bRRqFiPGMMDAFnV9RFgE=9@7=S5>xG_U0Z1Ty)U+CGGx$|V z=Ee75(lbF05hlpXg@T8NHf3LJon`1*&6xK+{Mebx7}dtjjV(Z|m#M7-@5}rt3e4Br zQr`eFxJW#M#xJtHc{>5s{jTxFL!0R@mZYhS<@s}S>~rqtT=r@Ft2UltDUDM3jBzYX zz9gJe$+K*=^2zJtom=P{FJ>yRZ~SUXgG=CuK1`gU!>w5ps;4Jeb?rB62!l18vbl-P zX??m6o=VC{gxbR4kAg~a?{0~>z(Kdf?H&Bb=olM|uq1ayOH@m4*Mv{$@qkq*kitN- ztLM|5D_;6D7cxY2yP29?$ct3Rho!VhDb|L5*Iez`&iTq2EJhHhMvIa`FR+Q@ghh{RDG0xPYxVMnmwvZDP~zkP z2cEFaUV%t{XJT#+n_ZD({KmPJX^_X$rn;)CiEW?F#F{H*XK*CzC<=CF!Jp>R-Mvs~IW;;} zs8mw`?vqatH-y4zo&jT{`uk8jO&M%VH*|>zGow*!R|Eg#B-JILaN09EYdZb>^6ul8 zU+kjp98W&hfKF85TruEsdTelHqv|)&4i3KFiMZM~4yjfb4gN7IJ5(Eg*YE1};U+#0 zA87E!HQNy}_61>F>XX?vfA5d~cI?vC)`Seffja*A{5g$Fg_vz`5sx>FslNwj!_=GU zy*vYY4y{mlB!jdm#Hj0j+(Q}5hn%QBkr`#DARk-ARCM@2`s+(1xNqj0wwKK|-`Buc z&wY*pCI%J=)uZP482w)&Y(=5GxsyH>EVwkW7|#K4PxxvSw$K+;+fw~-yAZ1LRhglZ ztxKEgf!%LCnBxfdA%^;n3Gk+wknsB*TfEsCprKC-c!8XOkmaDUB+w#O%pR?;`xsXolF9_A<0VxE4rLZHa<-gulb&p^Zr7XCOa!!ySr2@Lp>n zu|iu}&q!X0W^; zyh+@hqx$hEyRMqV>9gosM%Xt(r%9VIN41B&w(pRbrUJe+k>=d0F1A{+f?WZ}M1fq+lwe<+@|D#m3vx7CRLi}}>Zc>`Y_sp@;{M^E9sMK^OJ_Tw#m&oZ@4=0K z_5GwCNT>+2YBwv}#DyK}sPBcRV7nj^0E(R`6#w)TqzakZL#PNsPe3ZO z&k+=041vYl3a(Ibp^zZ#KLGV)Pni6vEQzf!Yu366#rua`Gwv$onLp%a_}=mkC4FJ8 zM39GYE^5NSw0%#F0P})&`_$InQhV}plCi$zyXxFdZqkkwaqofke?K} zw!*4EpTmdyFsi=#Ur=F|S72nbAo@1ZsXb3n2{Pl5{)Dl<(l*LtFOfi$3k70vNI9rc zP+Q*=|Gx-MuZ;Th_O!shwOVY^rzYRgE*(>sM>ybmG))Q=kFH-7Q68Y|QWVnIv@trQ zK?nKY+DiXBH0P*SgcK?*kNaB2E&gJfqddKwSP(F9iY!MAJL5uLB6t4aAkoljz&LgN zU~u~d&Tw`e*7)5D{+zxDXf~-y%*5_My9oRJ{yqI>$LCYumBmdI1RyNHDF=Z@F2FnbTiUX&sDe>=($AD?g05mXxL()Eb?bSL%@VE*Ua% zC^_9WIeLgMoy)`+Dw%1usBC-3IFh!fGxJ(J?h3rJVWAKYGX3UK)C`Dkpk?$({+!!= z)ntS+L78)zMX9vR-CSB9S%AW~*ftiET2gmoz{mH)oua+*Mm7dRBe|k@J0c8YBPIy@ zJ=|i)`8RZ9e4i8zXnOOj+R5Se$B(H64I_nkkr%>_`t#beTe?@EY}0R=C3n%_$~{xdNtF z%v@-oH+@C`r&|9}WS+^ND>UB@dx+33AlQ6&lArPnc9q6ra`>b(HjfQn82E0dlk=wR@yz^6PS>20i-`0BNG+W)8RBX6u{r?-+l~hTPth8#tswgw5(3ugMG7FCex|Un@7M z(kMd>ndH92542MsG5$%*v-av(_pb7BE;2x*ixOknPOfq;#=lVCmC(7G~9|`T1gLVv<+FR<N_*4F?3 zxd7FMg}J$un+1z(GeO6{QnHWh;;~F!emq4dCVoW|ct!^VU7-L?M@YmUITLXEPSJEe zr2IrnOP)VHdIp;%Ez2mVpviju4w0J$S-O$g6wP#$-i8Ea}_4b>L0XLt%29A{KRvG8a_K$cFi=|oS9uUV)Og( z?tx~;R@3!w*9seff5q`*rcgkI=fLh@PF~3-P!8LCBgdkl#9E3!vWj3?5K~#RDaye> zB809S^F@Wga5is8gnhd5iOFWL*oW?LWb;3!+?o_Tct9Q4u_v;FsId z_1{8awP^cNDV&jj`mUEeAvswRQ271a^qJbpU!uUWMP5OZDY8ZerL!y}jvMvPFg=fA z@yv^^pl2Q9^30LCQB{Z-QZ&q8l}Rc3HVxeKs3~RXmgR>f^l}ZvZsv#VN&ao}`D%Wq zAepe@9N49@dFQ5d@)L&kzj|t$stu4~K`2R~#btD~t0?GzkIS8CP`XK>EUJt-X~K>; zy6okQE^4;@JJCF`dHh_UsVY&iPU**aID8l)t_ga29(Zn;0=^g9caijTbUETAZoX{$ z=WS06soZu+u*0AlKR?*-T3Y4adj7&CU&jYL_og}xwm_$OeFW}TpuPn-6m|)YH4o4v z=<2?7TrMYRp`UpFBcx{$+(h0DCmIw$rIsZ;zi8<8Z7D1as;aIYIPPUhY;a3D$tL0? z>8QL<#Xr~WtR)v^@ZN$3L$uSQa7*?PZkoTx)Knuwqg*%~2unm{Uo%ZSmcVD0FV6K`{<`jDP`$LtPt zsGe&+rxWHWiM>BZH=U~OTlx9?j_U^#{fr0SKED3Erff2MDBCb&`?JeZgS7(Zw-w+V zx5NW)TpwFBeAYlfHYNs`Ic@4PXwqd%C$r0|Eav{U20a4A482h{*5Bsln%vIUG^JYY zUjE1*b15a`CLV8&54-%BZ5m_tTpzCs%~an41fIQ2OiU6+l2w0y3C5o4@Cf`n31E0`OMhYSVrBS(xQ1CViP|0rLb}g_+r67!cEDkFEv|T2PP;1-7$Lg zVCPRdOBP_|^Nao9Ic*Hr5I>U=9!S(+ybu90e~2=)hO)=2vT6AAkp4sd2o}NdnLW(+(_1 zhiUry6=7f+giWPw4<6e$8=b8*s#_{4VFR3#B!GF_&3`g+FYt1$z-qMtTHC*RIP`)$ z%Xhlk1G_AfFO3RD>{VD`Qf@QDXl=e!mesfP!xlmgp9NwG;-7`3%U=G*fNwgbI_ZON z_|wprz^ox(t@}To&N42_F524+3@tE-bd7*m2uOFQ(g@NaQqtYs4Fb~AsdRUDH_|2D z-JI=n-t&Ib5B`LSd#}CLwXXkg-VvqR+G71s8oU#PmP)zCUe|m$Mac*~ylQ_B-vAh8 zuE*o0$7Y%dLd5}yY1SSGtC8FHm;B3mNr@!ahbaz=x>VW<3POP8H&d(Jp}zr%-Ket- zPMCv9>=6J@8J@L{+(XCf8c(ulk_5}6x*pch{{FtcbfsSh>v_A)SDhiBwA^F;PI?c=`z|A@Y=6S_W4Tcz2-{a+Oton(SnN!xt_iQN&D;-J};6GF&Prc6#l+>E>8 zHjy_PlaSE)m3R|u@%p+o>e^$IaoNWIpsNkSz&J5JTx>M`4kEx`O%(~dM4YBU0($Fc zw*=hXAy`C=p3+mmju7veS;#?==ny|bh>t&pC(DByF2wJAX2jMxJTQ=!Lkl}wr1&UU z44;TidVvj*oo+Gm=0js8UJJz2_s4+V6F+YsV~X+K|DbQHKTyG{_4!7hB6UaaIhbN2 z`n;f7S;R&WCQBz*vmK|KKT|~AMu7zIbGo((M&gQKckc>V(Z^AJbcom_QyEzJXR%3HlMA?H>KBBQX-Enz*;IVV@j~Cu5wn*1a1B{`L$AhYU*g)2KI>+ zP?F^R9pK(Y=>QS^Wt5+g;s5qx2=C-^HUnomla9Z`~WAHdhue>iEGvB1Zlrh zG|MZ6gYM}PXEHYJD5t!k?Ex7mPTokg)XgQ-h;K?B8ZSFxd~pFkM>Qm>r>uj%)&LV1z}o?6H5uZ)Z6=L?;{t4ZNs zsThZ>f;jLMH|lAP$Qu||oj~N*nbU^E9LL0xbFg7i9#EJ348uQfFu*VL8&Y5=HI5Ksq+ILmlx_$S68fGD7|(lvD=w+VvYG5c%8hs^e_YV9KFu(li2wlU%%r z(mC?y+wz;%?q8K?i^7(02A|6N$3A`rPfmy>#fDt*P1>Av^V_#?x6h}S(!l9p1HI_# zLZK~Ve9rK;Uc#dBob5KzIcA2+dBv!RzNgKCSYuRS(hA}SlQS{*CP14$h{~z zC2qiqb{`FenkDQKY|vZwRYveX#7ovq&74N63=0`;@Zat+%Pd^nA3kzMTbeG_J5YkX zo$xA)B^khsW`fXYPvze=lPA&+rHqi<_wgee^$QgAgju{VUv@vu!O-1h4oo@W=@LW~ z#AtKe_y{h0@Su?poI73lOt&2bI4nD@fQI#ztsPWc)B z*vYRla|jq}1>ipZKM2jHG4o}VrxyuJY;HYE+fhdgUW{*NO5bj+PLf>MO?X4gx$nZO zx(cYVdhH@!=khy5GW#MM2P)GiegJ8=wp$Y~zMbB_d*?tS&V?FPNm8pbtCGlRMT1F- zo!TU#jw9gm487;8`xoMbgq3vl4e1>8%n!Z0r{mMNGGMs-_Zg#(+vsmnEIUp6&iIK*{q_H zk|?NwD8%DX<-48i?9NTOBJPdYtwCaihgZMQr|YG=jq2aV0lf{HCJbA8m&y|+T2c$h z`8cC#m{3V?$rgcus4plX@378ku4rY;*xPnEK<4JjylAHDNKnZiE4S3gWV)4 zq=uoH;uhKhhs8_6+WWlu;db>`EUEmk^MZE!GM0rc!iu)2s(_wN95J+9-FFDl&J+Qj zREb8IP?Y6G8KVQ;@U2TlEqmI*B%Z`6b8b^y3sh#U2vTv;uE06{37->rl%!qtTrafD zCKKU`ADHL(oBCr8lns7hAQ?90Y7;4=Nter(%CH(@9%bt)pxE@K^_la*;tM(>G=J|3 z*F_qVPcgEou9c~k>!lJml)M$m8?xlWghijq^B+%YqqjCKin15PheVlr&?EsYL|N)O zL@mgl!S8~u7LV@zWbBDGCSn+WL-Y~4jV<|aEXWL-=?sy(m~QF`Rz%J5^8NWALzA|- z5=u2&9*@RFf(KsRHYk_f4hv9x)M^TF)x&SMMl(%^!r43>sb7)1E|=oa+=HN}zYeIu zt?qlmF;1`}U=aQu(M`Enoa%ZaSu=Pt{|pT(8GxUJ2J`+0%Q(yX_;rC6{TATl4ZN|- zx!2PQi;8l_jX*1YzLoWsfMO#x{Qz0pM6 z0EO&uvDSwN0Z$O({qbEP+5le0z!Uv1g)AK%bI0U4(_5eQ%BN36ZKJ9O#Z`%a9vy4( zOVukqCaKT8dil0pt=vDam5TxNGTn?`4)NMwVBB9B9?D7koLGG_X@O0$R~1x63QtB8 z=f8wKDb+`i;AffTR84SwBK~l$u|yqNED~t6nx}BHnAy)c#^_6GR6V|8noH(Gzyre= zYiUEgnQCMoZB^7i8>4K>5MUGlW0ruQ%^t7!dEI~ZHg-$Ahdytwuxj7-Qr{S1SX7hM zHszHy881 zR>wfKp?0h`X$>@2u6{rhcbKh0<5QpG>bAErVg^o-@GS@Zn9B$%jK|~O}C^mC_E@yOc6K{IRVf&-0@rL#-vf6EIo!6`USkC=*Z67|cVp5#l9XZaMVrwgF3GX^kWCtfY1J)%2 zhkY;o@nYSK2j%Z1)&dhN=sa6c+QRon$T@C6=%%@!3!EU$7+W-S$! zmW?g=#F0rrCDFj5ou~9|`OGue@^F*v;pO!mdDZ9om>0}B!GPK8@Xu1kLsLLh;ecQ6 zf&)wp3k&NvXFwMuBraVzpsXv6dTEb-%E-^xhB0WUzc-^kZYV>RZ9m(sn_&bK=bfQ~ z+kwIhu=6s~$UdC$xVyBNh67$J)XxK%D?Y(0bN3UP>|==**rTJ&akU~wwyCv-=x%lT z{3foH%`UhL?rSWwZtHZcY);0I|mPmTDbIH0g@d-zwQ@u(O;w z%yWbdHak%AiJwcq`xKvwSM`*V3+<~bz1gf=^4#b6IB5o+n&hrn2M7d1CA% zrMoD0cpvsJ@2gpRG;*;R0!6)$xNo~jZM0d^gIWazW?ApsQGiDzDN#LUSs-a6f=oe( zPzRgxQxOt~^~QFkWUGnCPHrGFk23NFRh@XoSF99{oZQh-8%BGC0SO;ml>;9fzI6w+ z-)qGhwI7n$Eoh6Y0eVwIG*$D`*AfUP+*bK+KJ3!(Pwm0b-n#~ zyHA-{RuuIC<(UFNDNQzx19a@JL^{qy$1T!Cna$IhA3JYNFTxD}?Db}DbvEC!E!62S zHJ*23vzb_&qHc9u|2jKmpBjxVP$S|0hIW6_JzUy+Kh$nm(`14VplnFl6T8NX>Em4Uzc-P3G zElT|(6qHU}h{J1V+5ri}at-CM7U`tgIml`_+ClO+CTd}+LF8!&v%8OV^;#&J+8cWL zIb;y9N@;uRtv%odSX?LpGkjgb1nJcNYQ`&>dVc_vi}lq^Z+B|?+V|5|d_wXw>VEpP zNt5u#fI{F88Ce$VTV@}UBy}aasw1lY*4YJ%i_|Rj4cRZuyJ{ZH3$ngtyu%!t)LA|? zyOK5NB6GBAC1MhDgkYqZKfrCsHI{&5WR zT!(UAuz~+C;eq0=kNB4 zKXpz;0waFJgbz7}i3&fHYNveH(8i!@HD5dFf?K->Q44q&kpZ^$lDsE%UysTC`b57V zFE6d3fk&-G2O{neu^H!J7W>D8>l2I3)z))O!k-9&S0~p-XCn|P-^N1szSA1jHLSAv zk@>!LrlO`g2~UfWk(NuCe#=s0 z=bTsqF+J_Z@6jAs?hD~-7|+&(Y4Wc-u2d`e*Pjb1OFw*-{;nt*ZDDGr&kmWYFiI_ zw?=xI*aSV+tFF&dCO;YvN7-mF z(U3NsdtFo)dNnL6dR6J9xh{wHwglH31=`i$JETK-vr{KV;R3U-xV07~kHGaTg$8xk zw1PUkHhP8xaxVhC!SxAI)v?OY2&ZnX*Z65^>;A7VL|fEtr~w9lzL{t@uqPisR*spW zIvHU=8?71CW|89&se-u!LDz6cddKU!&p<51ox%$@^@7 z(=Sk8S8kgI6$>l zjZ`fjO39_!)p5EKd-|H2*=I;Ygro%JLOTFKAe&$D>q2NCbQK-XHon^@vZ>mh&}`8r zdGJWE<(f?9uMb=VGV6ZCf(2MVZUWiN4FZNqR+e%(NYg1}=BBL~HmXx&pM`pqq0)0G z8vJzO-g(nnw=BPVCO9S{?;I_0)2fz3Q^QUZZ=u@Nex~@LD7*a1QH99S4f2LuP9kEV z)U)dec~7@%KH$o2+#VFO1&zA@f9kuYSeLPwzomF?BwJf=4m_wKrbLAkHN{%w$t9AJyXY(t9Z?Pth zZI7pqWVT=jg$1sOW zK>aHm68hd(j1sazq47RWnLfEq?cB@H>1@S+fh>hwgcwAit#L5yWr}rpZNlTxZhva8 z=?x#ipo1KJlWd1Zh6ertO?HP*N#tUN<@eujG~XV*y&1ayxLxh>NM42T>UCfCQd4sR zy;lB)m1pEi@z2_5_nW>=+x)U0GOi>d)+m^@Vd*zD4Th)e=Avx2Vevs&RL_=Y20Kd$?^A)R#DdD<+O)R zx%C2@NXp|vfI0^%SH;ydb5@^}wi`NB`0PYn+opD_n4ZwCp;;Fv0uOOD9V^M6T;Hqf z?3ns0UXifhxPK9k%!*<&rNJ`R{q#VL%d$-BdLTgPY$sQpli|K5%4g=%9l&yLcvJX| zt_wo`IoCM!nI<^tzbFmSN+TyJMg{3q;q2F77om*J&+jPV&b=Gt_N=m^(S7^IbrY3A3lCBEORUq%ndnh_UrI;E_Xe3?A@_^i3TADO&Y$xz0akoB|F zGP0zrh2@OFo1z6$Ss(fc53e-#H1v0OLxNq$q+cnreoALD3$m>F25*Ti$D*Vj;IQrK zehF_eVJt`S^3~>a9n#gJkU`DaZxa>~MdrMdfH<)s<6ZR+3*MGzR_(3{l-ZEW&?eHZ zph~{02&@=l${KoZj*7-RByWvfG6qAfWlnrHNf0q7h=g(4bBI%tqdx6}V3ye8fb+?% zK!ZGn{`(a6zSv-3eDU2%5QcU6pFxDj1U;WMJ1$O{6@zzI$K3Outu4v7KCdT&&laB# zi&QxP{P@;2UrVS{+5%)lUFYfcaY=J+zL#=srAv3N$~%a#X>d3tW3RtIsoEUGuh#U- z0^f9%&p2zFMuR4w#V($2^eb5VE-2%!5Rm7hK1Nz38*V1`9rB8p2{v1Wlf2B5!$g&? zX6dbyN?yZy#e88IzGyjD7d?~crrcaeYweaOvCCDd%v-n6cY@IE1AK1xfp1yg+(_uiR_0@H+8JNhM|q&RlSPMU;(51EB9#jJor1Gj@}M@Fv!nO zgG^@0T{D+YXH^P6bp*(XAP&D9^#IRzEQNn;%ss~Ue~?Uv3rm(~%fEBt=D!sbFuB0^ zXt*Yl`n?mh4`CevJk)^kb!f#<*7axzO#1}ig7n;~N79p=+1pH36h2jd>y=iYmYJ06 zE$QN-V)k^r&sRXFVGAp&RByQI6EVM&?Msh4FFF3YMczODpSG6lv>m7?eg9+!^2<+H z^#bRvq%jrilBTyiPD6Y^pW%c(uO0PV1I!I7jw& z$HwoV?h?Co_^9%Wm@8G!&HwkO&^yiSBq>^3gD1CO?E@d?gsSI_QW*2gxiI^|cFLx6 zp4yvHyuQYYsTR30BG9^95PLuu{pm{>jkwQGSGZ{6H}R2`9E9iHwW1?>1)h~eqjwWq zdHki}l_4+o|8{dTEY{l7%vw|}E&d#eC8t_9{tV^yJhrLnZKV(<;x7*$s$w3kNi++q zb_`_x5kB+-*JJ=M){JUY#Xg5pGll=SNGU35c@c$}k}>aY4cK(>03X|S zn_yL;+dR&B?$E^2`Gw9EcN3?}A3E{o%w(CDj|qRo6xQxvz+UOFp^(v^V{c3sDOHGrlnZYFUj1W0T)YFi%ZT+aM2oDJiDAOH{7I_`&04i7bTQ{7Tq_}9=LTp<4Sq62Fnr510h zxn)*;X&@0kZ5z?gtZUZ!4p)0Pg&@`_b9p7#F`=mf@W2)|rb=doh*|T5y4_IXu9uX! z1RMU(gc&#c|cI%MPe=nUM=yp!d%{%*8KRdr8(QcCm9mdn@XJi>B~JGkmjg{U7rJEoF_RCk(VaX~pE%M-^)d#(O42anV$=^&#u)!BAOA zpdD9lbdqdSUGy^y6leQ8uAR>o=iCcXM~N3J?w0`%*{dootO_^@r{*w3$7jfWDC8($sa#>B>o0yB&4!<`eC&Bl|;ii(bR!qMqP-)7fRrsbVx zmj2W|ozykQgNTqke@5dm=yY;>PscOnJ6H0h8X!jo2eh(>m?o5X_vfmVAKJL`P`gNm zDP^^qu0Y4S5#5M}<-7iyJVMGo=zySfAs+^m&U;ata!(T>gq1E#L0P7Uq}j8wC;Awm zMQpy6Yxe!pM!n}gFP`!Yxw5zXp5MoomvJAy&PYMR86ZYqAEl_^)00Xt#Erk}6Yxm5P!&WrF>( zW!x;3v$qJRL?-*Hw*1r!Att1y9pUIJA z4PSY0&7l(LX(Nx*q(^!|tt??;VlpF=dVk|1$R~zpw_q@V`91Y}s|NCWum1Ilha>ta z!I{DS&A^kSDN?^AOob;^wWQss3-j=QEU$y$$GzzL6L!T$(E#SBG!)f6hFA6feygnx zo6OApjnht7<;yIoX;(te-!e&TDYUXbrTzmPDvTB5TfnI8sJ?%#SLZ3W(Fk0v$(1vV zoZCO5*72CiY`!L_udlC7MxHD0ogPL%t-$hx+e zX^S-AV!oL7#B_$<$%I#T1+C#z5u-!%5qSv*IbQ?X)?3S0Vq=_iRN?w-71#>S!aQsH zl^cGD?K$H4k9hC{AINRs$G#)|N|r^CXi3h2JxA%ZF8^lDvApEV=)EW_PC9 z478uWz zhb6st*Palht@e7o>bs_x^5;D2aH2H3_copS9CPqAGK3*nd{ zN8>`{u98h)N(c@PuF){MO5Ydf%wP4V1yutB71f7-zsf(01HTvzQHt?#HossZz-lA_ zT3r^D0LFW6246VoyrjvjXR!#M{}3=E9CG8Sy#m@>S{fvtf7Y@KiH|hTP5%tswsQ0szlgUUpEa54< zF8)CrET?%ES4X-PCe!r)8cdSWv?o>|iE^b33F7gog!5lB;kh?GE$@FXJ*a??M-NBZ}O+4=m^!3Z8RK?W;s>nOY=jP4vS!6b?aZo06r+V5OH1RB8t7T;$S!_1w zXy+J<1Gc$chxM}ggm2S~#W6Ts#TWkk`O`r8u=PrN+DDcQ>BMb&>;+pkCA&EpCS+%q zbmqw;fOJ^&`<{_-T0@gsDEZyJ)zp>XTrct>D@(wShW&2?I1Bzs2Yw#6cOx1dS+_Qo z;9U3X-9^*+X6<`P zD2iI4%Axf6&}vS5v8BIzh2%@IFN%E$o)n|NJj21nTSduf9V5L11XW^#gYQg{VX;h# z;7wNuN$Zc61b?XW{t8PPf|AFy`1TTCHfc1c9kxt-@=0#nBWs=dCwsbJ4KS!nPxnH9 zJJ9ac=yG{-d3{QF%*!vuJ z)4F5sS4Yojw>WTU+z;kCKPlTs0WsWcwmmoly}t^afP?cjnhj)8Jp@@ZVoV`#g3G!7 z0Z##T861V#8;oZ}Q}433w*l<=QlB^dY8(0KzbeHQduXh+He7yL@K~^t-XGC7u^yu= z{v`m(x&WT9bRd~dZMxsLD82Y|cv<@6uFWsx3|RKZfQf(m-`-!|CK?9n*psJCcDrn} z^JU;m+ST!!2%UN&{N{*}X#+Ff$mTquDfK$d^Ho}inPBKCS!2rJ?+-vSnE^s}~G zry1CO25(k;TF6(hhfZ6v)q*ILM0A0|8BRCi)3B_d777F9-k#B*n7HqV3?moU%Y`8R zF636fy{`m%x!r}_H^x7_I^T?u3C{WX%8;YNIRZWnxZ**N(us8I5D3N-BezBJZ=DyQWZL8&2iwZw#h z9mAGH*3|OHD==OD^OB6S8;6XeMa@zvJov83argYOj#W;rDQoz^?2aKJF8;Tj$zzB+h&&P!i^>0xQ4v}_B-!za&C%nzT%9A$8hQ0 zC^4Dl&r2SUNa3#{SS(?$X8d&i%|4$&9P)xrW?-$Z*8YL; zuaFM{v)~odmvo;S9V)baBm#q@!;e|IA=6DN-MbH3{a;>@7q$J1I{2|Ht&-IfcbkYTjDBmwS@FK8m@voBnWb_Y^RdMOTtHtoNMwZwQ* z4>+>@4sm$RRc08}Y^m`H2#!`ME<+m&?cX`@U_Oh6nw*4HC?4f@#($Z&dTf36857ZR zAsyZIeDd@5eC?_Hbt0beAMQE}T+`i$+t1ATobGpyxd($P)35N6S-pn@wYdjgM;h>4 z9`m(PLdCB-$U0y8dIMb=MsqnM>iYnyHkyYx;OH<0$DI5jdyLl_ZVy&7VB&^pZcqMh zJu}=O6S^N6LUJrh5JJBDhp?hI4b6o+#E#Yca<*b{JMN5Ixv5-!cKF%oNIu71rhgtE z4chB(1aRbMhy*ybwYC3HN1Hklx<~=)iGggD(m#6#6~8l5b>WxxUF?t=|0UoAUj-r42S-We$QhPbVh3 zolixmbCpLOWFE0kjS)d+kti;Q87^dI`2B7>8<|TtdG$ZgvMn)xc9w)qF$L@*n$7(X z2N|7Tq>IM$D|9z%4Q{2!&1~q`h@=q*I%|lZeHfPK9j>sax2EXHded*(JT2L{Tv@cU#yAMb3p{ifcmbn8>l65) z<@A@e;P>xky=Br+&YaDs4}0g^$1H_fQivIwxZyqqc>8S{UODneHXkVQEShFE%bf0^ z+sHOrIz{lrdN=KkHlLZ8d6|qs&caQUzEL3>{9fK3?;QC|vE$@+?v1!+*Qmnr)C!f!*cFAbx?T>a03K`Yt*#o%9$ zdXiwN)N;!>np^rwnsH}_F#xk{Z_Zw0-}%((u5#GWO5A8lk-@H&8Qd#=NE<&gNG$pIy(RDg3c!CLb{|bWk|y0 zcUpcxt)ZeK^Coqd%9>+2UGe8^a;EfdrG$;QW{Px*#;_5f`yTG8Big~J{x9@ORg5?@|8#KS$MnSHfKcHh`mk22*F)GU*1K6K@A70T_#2if0WR6q7AbJq>d&J(TqxVPRTZ|)pJDj15Vf_JLog>VF&zn?j_&PtUP z>0+2ek^x{zj6IuksCeqSE))=n^WzopNTQJQI@8eEj(5kGOdB429{w~ic(Kol{Bic_ z;Cwijv{xy3!?O^5{{dINk~F1^f-+Np%1x+LVPrkTDhop9@LmV$8HtNdo8%xA8NQjF z@?MXYp(jfC0ysUFJBNWiaO>_GQ)*yiAYm}dRqzuCvF=m*-q-i$=4RJoYo;`_AztVY z$p6UzmPj=(7jM&|dfj%h@C?lTLqIi(f*Nz3DNP>o+LK{zrCrus_`#N@9=uN0blqsq0-6b35HpIBz`%L^AVqdQVP>&27eS zTx$PvIek!VS6D3Zg>Z090)=qkVSzm z3EU3Urbse1$U^%eC1rWfX*Pu@_}vl1DEY&8TQnw46>pP3au}J0Vk9IKX!N;UioJUw)b~9yv-_l~k;jsn@_XP?;N&fMlmbJ{HVW~QhA@t7dm(Q+m+71XjYP83}s3T0N)jFPM zWnnDFZNTE})+5Kk#GP-4Z7pkc&yOPJsGK`Y)nFQK3L{E0e}}fkE7F7C^R@?$ zrU8t`N%g`y?`^kWw7EnUSJs&xL?0tl`ue_w>x4cC>IK`;NKKbwsTtU9lgFeKD&Vdp zBBMZ* zL54|H;3g<2u=3-*>5SaadkuN{ULgb#?=DX8+inn`u@Y>;HvKEPt{d)tU$Va+9!B_v?A`p$o}QJ*CZYFAS{r8WEf&}J)89K;|GY#6R1gN z_2Qtse6itG#%NVmf_N{mhY(7L#*W6}%RMWKc#3&h`LmxW#4y$EM`a4Q>E{#_d@K4m zIQWF38XTwbHtN*ah<%gvZyQ~3FccsgCa*-ON*Zl9}Mwc@x7WQv7tc=E&XLACI8kRk(7PwvoIk;`0RVoE5Y!AmGtj&+c#eoR5{zFxcIfel;dXZ{ZYqQ$ zeXo5#i4)J$`ixuNvgoJhh1b+ZyP5z5G3Z8cK%m81s9PccHH*l;)ay$?2zb zQu?Sr90^L_W;GH4#}rCKpOV}52_FX%kG`*@>!%ElCk(3UA)Upv1ss%SUIe5NgvT|0 zcd@xDv%kBS%RV?KOL5vof2+J56QG1Pcv&+PsDkL>F~mGY$SjY|*Mk8Kw-fXzoZY#3|o*3seYVjB2N>qO0wLhuM28 zQPsdW&+B9Gt+XZ;206`;nCN2xwbj-yFA7KG@A5BW`Bwzq#LHmxtZS0F;K9tNU z6|76}MDbIjB}8@C9Lu@lhb^38pfnHU*F3{Bh}U~76x-|Gz=e=Jwe{=FWT<3=j0V{k zgZ2Qv! z5w=3{6{3gImS1s9VcrhZ(?%e~cZJ2Sk@%|n$xlKn*}y$T}ZdmS)aMA!1?6%kSKUB3}s#NGmRVw3d>;__`yIBstF5LKlV)AFuBv*O?qGFQf z{*FhG-?H#|%!m~|LTDKA5thEd5yDu}p|Hq)fGTgeFfn`w#GWBU+oQR`1(=bMg+^`} zo@W28K*a<| z`##|*>{zRJ3bv~Ln?6WxT)S8@{T3@r^oq!QDvr-Ea zLMj&=?imS!y1 z(|^4!Gc^W2l8cM1 zk_jWeF%avX_QoAhjel5jJyn>}nVy*N_rXcD!MXno+5c2qa9CsC*G;;Xy^+h{Iygc} z{sgDn^XQF2e<2oX>s+ZGHwaF9&a)(6jjfj3>qH0n9z1Rt9Q3{mQyB|BW-ym&;StNc zK#wLwd3G)jA(?FadLfM3%)I$sQy>#*Y@0>B&J|&pCM(g$1^u@N42BCOe>jH;yf}GH zaOOr+cfj-#w0q%vzwuMEe!$SaLmD0Z>@iZ0h*_z>31>br<05Dnp-QYRDz4VLNB@F? zMJ<6Fh|sgJGkIC6dJ5Wa2|6tr#A>oe{encl6`o{dRqo<`_wD5L{w#P@0-C*igkRBU zDDRn0i<9KLSS5u@qtGsK_gSiR>EsB!gIb4iJCt$n0-c;J;%CH6n3Ot9<6Sck6YpBi zC=$QK>+JT-aB|kl)23bd+IcdWpgJk8d0AI!_BFth5dhRGP zYE+rW5%9fw9+Dp$VK`HX%GD+ zB_ozt&LiU+@F4D*eZ_32>M$90BG4g-u*{B)XP4FBHT8sc7_JYlKwKs>qbOI!RN`E- zU%oX^d!kHIvK9I-Lr5HqkXE#@A<(-tr3d3l!W4ypZ}^Xx``;LeG^@B|j44)xLyv3E zr|_koFlg2dGZPtUto;<`t;)uo?ocSh8?Y z49<9tb~^Q3Egx;J9ynF(+w`D>jaRGP*buohs*lFb)a#m?%;992Rj;ucny}S`e@jiL ze3Fyv@j6=OLr6zM#Athp;Hw<%N@gKDn6=5;=f#TB=hw@*(^Baff>_a+C?)J^wKrom z^a_C;NhNrhuZ&%3d#K@r{;%Uew0*5nA8UyoC{0` zbaJ`srC$q;&|kGsOB3!0e>H1pFVX4^cU(6<@~qIQ&MkFzvw64pY%89g5J8nAHmVMXoZc4nRe-y z7bZiv$hTlNx6!i!7l2m=s{N<2|J^yICYK_th3Ymwy%zDXg`d2Dmy zz^ZT)?_DTj#61^z(ocpySKf?!)F9SP7#g(((I@T!CxYj$QDszfz=F(Br8~b;j+KEy z>1Z}vIL7%}ntS7=FSh|q%xp5vheZf4^w?Wq&EjFB+tp)PtHJsFLygRU==w|XvpYhq zEBeVPD&8cjxh;Mul!)OB-5crW-zq2LZwZ1!qh#byM%?N86s9oxep8r%=_BEiAz0tl z8{{1l@37l9#PNdsRu&Va|L&B#Y246_pX)m>?9 zW|rR8MnyoR(*zb=du`cq|E=}H$yeUIAwF6sF z<<-;tJNZEuF%xP?;b>WE?zoMyqCTBD?lVjhC8KqzLS=`AkT>_=Lx2h}A?3 z9K>fS9l4`QdCF1&x9%}9{iafi^*TFJO)^?Gl7enTcPKnY362BD7%B1U*;{Ik=p~aN z9f+`^7-$wGtIQ$NU*q&OyP8;20;9gB+L4}2iukFvW+D<5qbqp|mINYp#quS^VS2Yl zL6r9d@aZiEI%G!LkyqZYd`y!bx&qhT9_~0FI;slZ;2U9bmR0%_tH~L8q~D8a3((0E z3|yyU`4N!Ljea&-w<}>2@eGh;uiCY0zglzFah>Gcbdq@u5qhgmDS+l%%Ceb5!CnFJ zS}37B|6-m&2mR1~j?jy=B3v8Ar)1UA4t5|X-+({-v{0g%heoiv$>lA#ULoa~lccRnj$sZH28+?mO;O7Tx*D^j^ z5y)@-QDr6#(KX*NP=?#zjYAeS7tw3feS$W~Z)0fQ-}>y@b1bIHzYw$Hf<41+pR@V1 znHU!H7bqM_zAcq@=8YUNOIV+SYSOFSi!D!Gwdj!2E~G!u8$YIVyB7EOY+;{#w{*Lu z!8OJr=e@~uqcD<>2-6a`@7B~_?b~EM7H2b z0OyR!RJeMrOL&pA3E?tRy**WHpVoeyTB1(WeE|z$zdYj&wwln_YdEuOx9hGHP2l45 z#8B8;TZ_5%??Wo8*w|seNN+TVvE@FqdPO)*GxeUU+=sq@KIgXJ5VyQ|rg$zu59Y9H z@pxhJXBAQTB@t#Q;m%|!E*A+Uf)J@0Zf0>%kv7czi%91$##HLkh#=Q)m9@kcE|fM# z9;BkbwX`B4kp(r%Z%G@bb@KgwQ@|$Lvp))=)xq7o3|Z-z{QmVQX}6Ber#X zM_5(cy~*ZQ)PqHz6YPy_B!<`m9$)Gg!h-p@AwjcrTdrgi1?s6IuX)`eZ7FMYf?LUL zUDajCp_6wJ;poPd(RTw`)~s8-jh3nDKN@*6w>ia8Zl7xD|Y=kwJS!@H)q~au#8Sjb4)pHyuT=;(hI#1@ujNMbiOO0 zOeD0KAGA-LrdlMM+MK<$2#+ZE*ohM;J~YTSH}{oFnO`~%ykvD)@PVGm@HESj45UCG zMzdL#8xV9flz(#;zr#tos};`5=S=a7({umLqL=wEcrA1x`4GrHSJh%*z-8i=kQRai z*0{X`MfaRZR62mA`gm2&#G;faAZM^i0`*J6f=OZCqQOH|dy{r^nw}Zb-T_-8O~+H6 zGq(35BwrNeaEp0!kiE$_IJ$P!ZD~$>IR5zY9T$lk7D@g#BTvcf{YJMEExEBj_hr!L zw#tN8Hq2_ZHuo~=zACz3#ErfG!-U7F^Gl}6&}mvc@^fB-3N|uhJY1OEIkN<4s?dUF zDBT+gjfUG1K0cD5jZ%1Ri+(3F*?Y`S7l^e~-V;JJGdV*S&xf(S5dJMOX2HLoe;>OZ z;a1D)HqkS@Z>m%1sw7L=A@MXJjyvGW$sN$E-&ND@=h@wJO8K)iX7pzMNYJ_;QMHKu*x0#5dJc^bDS=4_n4 z8o!8#7@F)bEH(UQvix4g&DNmF-d2k1f2cYWaHzXC?zd1R3_?XCTNFm>$&xKwDk2p| zcCut2vTtP_4JNy?MT^S5k3Eci8M3dV8T&r=-Frs;-|Ky!>v~+5F!TGJIp_TD^SwX! z_uecQcyRyd>4wB_w#9c}9(AJwo>1XJ+?L-qIc>c-#Y}deK-#~;#WL=B24EW{zuC*q zmlO3r&>hg}TX5W6t6Hc+SlR2$Bb7U!p%tkpyiaZ>jyC)Kkn46w?e)?8EGeA1A@6t- zc~AG)C1#3=JDWw?0QP$AQWSq+^1aUL)Ft{%(;n$=gD0>{*C}Q%_GpsLiwtPN?wjoBlM z<;u^QWgSwBapx{*KAA{#>fo~A@e2qDc#@wrU^SdCojYGTvU1@9mrON4rg6CL02{3Fwg>3$# zk=z^fFdiq*K|Acep>2lBl97L47K-ar}+;L1S4Q}%(Zx% z7iD@Qf!i-+-AMFCW^#RA?I{q#)V{24vZrVMli)||MVyLX(<*GKH}>w2SZ(7oBeB$^ zjb|UQr4V^!g0efOIznK)JuS^=RInuhEikJ+TOZDLJ3Qxe*nD%$p||F`>#nA0xF_;C z%Z+D-0S^Mfn~@unVQs?{KR^gta;NlU!5@LIJxaUb4A>KZF#l6p61J#jgl*D)xfPCU205t!F~; zmg5^o6Qd}-Z?p)ccs+I$wS!XC0^hKh*=IjJq72tY(v)Z|!>jY41K=hq(1AZk=;TS!ygjG&B^;{w#LUq*S!c z`=W1jTke9}w&bItaqa+q4(iv(jSn>*8EwpV55Fj}GaVef<0X&*M3bRx{c`=QjrBJ9 zlmGQF$Qlm)d{LJ4+)un$1ZxkhNHT%^9h;P7>+bGe?73w(T;*MkBDf7kS_ByLEHJ!B zgLWoD@Z3%oVS~in^L*&f6*9bAVe8+k>_Li4x@0&DE3|(KzwNdENMVW8O5Qor&Rp1`Kcs=5OBwCWWZx%&W!eQPYD>L zVNl>~19oez3@ss!t&exCz5Z}hIUA*#bbgXEi1pO$VyEG3p<16{mMKj;{_vhd?b_@M zFjR{1ckIX7zNsZyJ^uJ#9)LZD>rs?_Ro-V=EdYtBEa@qVyLJ1vo>zTM{$>2oxj&a( z-_qGKGwZLKN)h@4HlxsMs`@~sFGLUGn2qd`&@{NNVE;I^!?q zs8_12`?HGyu=5l)o9Y!$REc|}SEeGkLACh1ZwB0m4nX#7>Vjxx_zD3+UAU_*QcRK4H^%6eiA$5sSfPQ`v72K?U24u%uf2OS0W#N@sdL*mm?{TYkSq@5 z)tx&B+sy;wxbE6i`(kP^bk(zQzyG^N-R{=11&7aes_=m7ht)T@(b_=6*g0+|65v4x zfto+(tTmx|HGp4^<#;|IZ_w5L0NCCpknmpsF@F&t7A3%zuIx$OqZjFFwyyMzbB}Za zF4ZzIAm?xA5KpmWi28*(QE6kZonjDq0e5SL>EH0AHIvupJ-B6mlwIC1NFwqr01Kz$ z?sOa+@F7`p{%6M0PxP~1xZvgqFG&T%5VF?uco((uAA3pUeoK*K;z5-yVwrf@j=-r4SJ#b*rxqrWGa;h(%l4feSFKbv21C?;ZirS6L z^o?A-E9tUe37joRk8br*4&3AnNd8)DlU_4|sqKl{)Qgmg5h`@aW5TT0oNC793aBORa#pbJ#_Z zTCfP0OkyQ=2q@an$!96#+Z zYTkBH7Ck=ovCMh?nxYt@6DPG6dgs;e0kFI#StbC9g3ZEcu<;Tp8_0Cio;bL*m%swC z800O6rME^F(^vr3yZ{?Q8a24DPFzQ~-lyp1gYJQDY{0#!%t6Tp@43ulJ&+rLQ$Vq_ zFS6mcgSKp2W7}OOnlxDR*HM9#4V}5hr(GF*S8A@0O#zUiqPh@eq}GuLb`%pAQf~a1J;V7J~-v~FXF_1$G);mo~v1p3-O zg6{$ABRib*mcDL^fXa!RHbi(K_AV+80s|XVb^d^u4M$8L8h5V5j-D z=s4E~MG=jAOskCC??5A63nU{38-ml(lD{&ytv>bGt`qlXQkNjJ{R&Eh?6e~}%Y5QbG zSP_FeYG4ZdR*@aFWt88ET1(mY7~ zeC^Q?wJql_ywBd1WqW+p!ASR8_y%Z|+qx`$@HJ^~Ib~rjtUht?;lSyo-A8nM*j`Rn zS8{*^9*A7D+OH@q9G=lNo@Q5wk9>W;D-d2ElepS-i05HL`Mhw{X+lDIwd>9q#y{`~ zV!MkN0(+Z_wHS*n;Uh!AkIj4Ow3&H4Iox9ja1#Fy9eY=_avS4x=Vsn4WbPWZ9m_6GF|kKgL7FmE zW;#uDID1n2}{7INe`b z{Jed*=A-^#ie*duD(97uedHA|d;H21K^N63`L|Gev1ntN8L^EukFg4O*gol$(SjRa zZf-4$3#hT#u!ghObQ2oV@Gl6PikH}qm)1{yxd2WU?M(Pm+MF(a7kGN*CS4{(Q&=L! z82vbVz)n7KD)#&+1_aup4f+{tYdBuPv6r&;J>T;hZpHru9$>lB6!A@|^M8 zH*S*{Nin0|Ml01T(F!+-v1%+XgkeO4VWmfru3g?Z3bv${>$B<$SUTJ zG-4&}iVeK_U!LNJ8E?DU?3aS^3gL(AgrbgkA+OAkjN&_*2lZiB>VYy%34WR3G zrb=AL7#|BnTs=*4SD|6s$emtmO~wSSMfn&;7?~V#O+^9Ikpf&9HFDeI)2DKTIjia&Wcp zA&;181D~kd%2n1+RE~PT8TL86R^D!z4HeFA{yM!i>{_*Ur6iwQR6ECjNIf(ohPoiT zUE_<$+uxnfDNm5HeG*QJYRU#gV%6jb*o#Nbg-s@SAo}T-_7JXHx%_WGg)jessza3h ziqAshZOC1?rL>Zt?QJfqPf-6lNF4CUTwV8PKY^`^B1s!)ui{UwT`ta|d4k7=5WQ1* zhn%WD@`V!Z{T<%q;-zbo6HoWZeLo$006Bz=%;m`b<|l0ORN5V!Ich_-&3(|46zext z&3CeT@i7KF{DyEp;CuXxis+*m#-lchxc_{fk6?tagRQNEjgu(8MC2^k-8iIsSgggw z8tzvAqQ(AR<|pt(bF2w;1I}qmSMu&>y5|1o@S4*tf>}CD<5?^c^|`LdXo=$9N~G>%-S}r)7^mSdh%; zbHC5=d){>p_+lNp7|7}~L)dVw-bVR5P>Vv_0|*}_cfC3a+dq<{Z$)N4YdTR{!Yn8)m)uh%zW1J;w#>!D*Riy(rrB#tnwgmsB69ms~u%we}M7u_vnH z4!lCvI5p9cBtj`iok){MIH?HTjZkQwbiaSK00J2bNkLEj(tRZOE7*4Gs%8qoiJ9eI zEp(2Q_dXG}&MA|;qB=|x38HviL5DigjFM)yutVQkZ=9!D?9FaxkLkdw`6}QEzGrzz zD$vgm!W_Tyr};Xb<6~V6e>1-UmwXhiF{^u0A1yvyWSw6AhL7*wXVs*;MOo@8232C) z<;zvm)ZKn1Iu*(H9160(;}EV3&&>$a1eV!%4H1G(;IDHY!`P;7XtI&qReN(FbtzV` z?3Xe(%6yq4tVFI}=JURukPCB{toUxz4Cm`>P#MXDhOo|6lLU( zAYeNKS)O8gz*}=EO%f~D$az}4c~E|#a3nPll1A}Q7$RvixpvV}W1qggNl&dQ;5f-5 zkNk=J_X3z~tZCjePGb;O{)qUeYxT#lG4lz5eMOQtLHXRVxCFiszG+mX0N3Zj+Uv*9 zN$_QahsI;>3zU2z;A#+S5eyn59j7YgrD1u0!_B#ijDNr8-Fd9yfj2@vx)*emVcU0n6^bHp!f^)(IGAC& z#p)z$`yTPCa8bKW_Bd%=X)ANMgq~AIM+ihr`4i3Gi$uNME@%A1hj8_?c>;4fah zUYgqY(RA)yM+{?>-2T@&ZGP5da1Jm|S5GMh2czigPDMPz2KV9%+S=L_lS=$1Wn>yb zH!&GHBRhX1bc2=4pIS=R1-VQo8Nmku`CkLw?xaLL!Drp%kblnjHkW=7V4)b@S!fkWO0j2)M6jf+%3jmE|@t+_eLU&qf~5HwHX5 z7XYN)>zwKXjzs|aDxaL_Db&h(F$fO-g0e??KM)pXnk8eEp2MJ?%LZVJ%;B0;8jn8l z`ED1L@scNlhC2ok(oTR40WGiy6qI9%c0m1^3(4edZ{`PUP&ZHnJju;i=Rwmtv&>Y4 zwP&s!x1Ya?hb79fhM8UCB5Py~7u(#q{?0K_zHD0?K!ROfTU*or0u1UqeGcaF+k2fH zEoTT^?DMiHI&h8>FdJ2KnN?eQruQcJ){_!D{rS$+O535r!H#6){T7MCU0aAkZ4lA-m+Q zkPFn122ft1Z{71fRr+w7IMx7eDvPYHh&z0$JUZN~vRnh=-;R7UI3h7R(U4U~WvDwA zR1Ikg+!%`3E`QIfk6evF9g8K3%x6=z%3W+d0pXqrguqQ;*g6QD@v}aSr}h5SY@O@* zK_rMH?<$I9k09@5{s5MG!o1rMTcn60TN=G_@J-_7022pFw65-Tg&hn^Jo$8?EP_BG z>9Zd|E@U1<1I6ft{~6kawh!OcYtFTuI;Ay?V&xWjG~Bh#wY7kG5O_2EssoY8kJy2& zdYq%7$^6bg2S!XmeUbo*2l5W8S)KLo7m z7^i1hCqLY7(XVjp8vbNBIVvy}k{3`)IY&#O{d&&ye5y^pW{L>m!wMJ_UAY=cPayOx zg6Sr+JhEohn^x~HV%o6l?Y1*1I|SVSM+(y9{DY|H?`}oD#cGRysC8dvfaWf^a9Azm4V7R3>AMDZmhG@hY;v+W zsCq6cb($V}dwOs2_t#b!1<<~&z*KRju>vuo{yvZ5(Da*f`W>vf;a+ymy6o5g;gmU@0fk;S>oqBcgXunPmA zYQhH8z`!61ZU4o-zIRp3W}xXlD1!%+kYu`U4~{$`fmAKT{IP0Z*$mJ-fdHSF5skK1>E zr1x6eq4u2RSpli+RVosOR|+!^mPQlMeJ4>jsPJG;4T5Zv7vjy{f1tc`ez*j!yTPzR zmj6g0jo=t^>N~1FXsF0(k+pjJbC!O2Uru_`!5;l~ksHm;^Z8By0#!8*inOO|-LG?<%68kj6 zZ(zN?vg^mT2Wpa}8zi{7E1%;#Qe>x56nfo7*4ia=PP7DZ%y*`vgNZ103`NZ@PJGzKP>>QO z2Q1G$>?*#85Ykla%b8uX?!Pukl6DyXM5C2Sgg?ov;OZDEd^NA7ojIvj09-^^<3NLj z)m?@!m}1jc^@O`7SceiA@psrp^7aByEz56)(!5R<1DOUBjC#u0QV{wHmUn-n-_Z3> zh`=5B3-dnB#a8Y5@xRkC_Zshxd{yBqSqGwVpOVyKmwM(nJpDS_Ie{`@<*uOCcuTn1 z2M-A-vVxW?&U5Hj<&}P?fd7`(Lr;@DHf@rN?C~A?)t~2o+wQE!*g4z({*s5Q3t{5Z zlR8_n;$|W_m+H+T4xT3F5)y;37W5U#scDAEN{@{~j>8pq-BQP|LIxEBb&yD=y830w z*xd=~_R|#&{(m<5j}}lOBmU(}WvyUON`k*hZCjlbuo=N1z#5bm5UiMio&d3xBkpu* z2@OfR9MkG|9Hwe$QZ=_B#nd|PLhoTB*cWO8*FtROsT^8@C+s3rTnfe9n$nAKt3SkU0edjLCb&{(iqm=v{~tk^3pbsKgt48y|7WARe?Bp zS%^n5q&rQ9xs=yystq?fdgWvr>Ael@ykirLL+V^DqJeeje06H6sDOagX?|A09;9|A ziIYXDjlm3or`K9=%XDMcYfNz?Am#@Pg0~V#f#RM)$oh{wzJh0rWstDVN@(enA?N=CATh~orc%^enwiB&a|(N~=JZ23YZXG>AaM(_;gjrdTT1!M-7lb3 zRYfzQO)*g@07_d&duRBr0JAYeB2%;R_ZyI9ibQR{`Ty7ylQ zHp2UL^u%A!d;;-;ZavF3zphmOA1*YF=L{=Rt|wW-sjndRcHe`S2J461Eefl50zAA_ z3SNq+`9k(0wnG6KudUZ#P{5ta!I+a5|4ZCb){RP0i^Tuss@-+8@N;bCB}je1`P_p` zA*&CbD*NW`2N!=}id^SR3)$nl=G~v2v~-KZ-k(G`StAv>9`61URx4`H+4B~0`^?C$ zyR`k7m!aOP?r%ltlrOG=4v<#0{AzwX?_k|AVpF9)GUuDyE496U(SRnaYw-zg*e(bY zH`lw>-Gdw`U`wM^RSjWmo)UW948boMlnaUP9*>BeLJKuQP@Esw%^VgRZiHh%Z@j!(mXSNlJ;QJsrB~vAia}AS_$*3A-xoz8Y=M z9Q0lhbAM^Ii%Veup^ILJJF3D*ZMW5RK&JknEcbvtEd><2KtKv) z$K;IZ(mW$Yi=Dis+CB z!SeoduFe6vFbX@W1<$779H$)m)Ea;{G>ox7q^C)P)gy_d=G1-S1eevG32lb!9QgnBDtQ3$1 zS(oIaHyL(%&?UdtXMGVT$I4xE2nXE-QWEU3Uc~}HNJrbWM4w#%rNha>=R3s|Ng$;0 z76Bka&E{v0gQogD;LJXiH&Iw~n`j8qME2K}MR&@VE>G_j6r6aIAS3d}SU@bBde1*t zi8}Ll7j%htuV$}0PIx%Z5S9=8NAQQB6LdN568O~U5=FQZ0fC{vpk_F;rYv(p5C6=} zvGLr|kadpdnZ4;0-?2tDh6=iir`JL$p~At>(g&a&dJCo^M+DR$HLJ@|Et{X<{<<$n zOod2~)z!WM=go&Xy45wZYg`2$-+DXRH<+sy64_M>EWe6iJ7yGk73-rhYcYqBw6Is0 zuvwZ%0E&rW%(o>z?XbV4h~GGrW^r{rBbY{2`XG@Nvu(zf2ih{Zf3w?tn*sVQFE~FA z-IM@H|gXG#!y-++!{@4M6b+K>WCv)-8wnNI?@ zu_Z>|XUd47MBTMp5QJVWEKRnpgbY*NZ*d4cBtcI;N{XJ=d#NwHGU1xoxZB|_NMx^b zPMLq@3evMaiSv*t&NGSgZsv&#&K(t9e4BLic6)mM7;{#ZV+NkbE8`tXx zdO^MadG*(u7S+wC94uH=F%PiWS44c5UC6pZA?6JFiPIkps0bf)1YX6bHlqeP!=0`Ziq1i_CI3kdIABSN(rQao%!wlxkX01Uw(=X zd0Ub<-ooqh~!eeUjh=VHv;9F?C%trItN-vv#2APusXr29F8sj{=*Or7 zPGHk-t^Y{d3%{uRypfvtwG!j$1wTQ3fC^UW+;U5}rAmcx=0Ed4O~)AWW3u$@Sw!L- z`}-QUehWcDGj%8bGeFniRoIMoH;3)_wDf9*1r5}%>O4=ky1h=5VgJTlBFPH|vk|zg z4B2jO9(vj?2iZc@eEg|yHvRKuwo1d55d3o|4UKBz0Y5*#q~Rp>B<6vzu!z3{J;McS1Xa~x_IwX1Ql(D+s{?(D_nvYPCqInAqYdW9aHGad+@Hh zvd=?+v8dleaA&{Qz?Ik8K&W5kfl4$DqVAs#2+_Vja=Nre)4x z%vzaWP;v7772mMVJWFX|D==XRC~5Y?he0z!9uVc4{zg%p+1wgWOC#Y6FL~O4`sfwS zqec!WZ3ERgchLBM<-_Nv-X`BQe{lR;nH7~yf!|eW9=uRwObfl&?>qK4 zpoz=a%L;lq6~lpjydAFHld+xtaU5hzasgXEDBrLV5`ao~r%|E2{& zRIwHune3$hspQ8q+@12TTuVN7Dl;fH#i^4yk*-{uAoPBq96aUk;E{(M(CC zj8N=?=;y$YUrupSsk#^3)j8_*m#hq>p~rs)0J=)Nd7Lo*BkJ6GdH!bQt*ysY2=Ar` z-f#{t@_QnGD!SL^yeP8PWykc*+ii;5X;mK$RcZ2!2&}~ikO`?{Jyuck6ay<1(WU+T z`SYJ`F^m+%7`-S#SdQkCAdla_e=|Dkh4t=uY;bFO_h)3U&ONO1O_kb0_cPAQk6ivI zu28W$f=2TBDsfzL$+24Pzq`AIZr|?4JHcS<1V0fSDPa7j1A-Nh+$I9mGw`VyAz-z+ zHou}QNAZiioZMUBk+f^w@IE5qPnz`D^7AL(>_*EDcR|B589u|_t%QvbTF-E9&Ppn&GBIGk&0+`H$59MHB+)U4(7CMW?P>PRYQ&rJ&F?=rWvJelnl6F7|#pj2+;dMeUO1fkQdjbo|(fXC% zbL|tN@t_!L9u`vfEzG5DH^6S{7zzZax6ekIJMchK`c#3b{ddKZ_nD zkqJ(y&$h8OIK5qw@f!HueH)Atj4TlB0SKznAp0RpYmMg{B9$kF$3=_y6gLj}y;Rv| zQ=;^#4+vVQ)BtFDw@5>&q!bY>ue8RR;~yB-DT6u-_`TOT6cucQNlYNAyS!KrJr>QYCvJqMV(iYli!M|&x_SWqT zT{X4G33ujchJk3)o1J9sJl8`O-Uj^X)evuk9?;9fFwaZ}>*Y}wJDQN#EAsy#7ciEe z^};UeXgzr{lXkgF2ab#v$W2u^JKXiT(FV-J`bn$kGMGR{8S@1_RjPiB_M~zt>_bvl;q;R(YKz>I1A8(gt$iTR@sb6oX@ctEaLh-NnwvGxSiC($b;@ zpbIX!R01B=L_@dlYdM&?fQPdLnsfwNP1G8SGxVAD^KOa0E^5`o4~QgSXVllechh_K zwIsmCb;E*b*{p!v={&yzX1^^^P^h~uHr&&VmT;bn26XYSdHH>CY^ZC!df%V-WAX0h z;=GCb){?X+usQ|W%#*gKQ0n!jb6$gI&R;YF?UY?kdhKsh1IiL;LdDV|qJrz(_d{2z zOiE?Ewvk)LGo7hW(G_4^(t1rb-Uuw)0$^1Lpx{7Jhw*#9xe&&7{3-H*%j6`5$|3MS z$aRn$3ziJsiGr_w`B@=y==S2#C!juD zO@Ezh$U%sF)XxO}9#TetbpXYtxOykSBJ)+ZIW(%HluRLlk8Hwkdz)lx_d^M}G+ffJ zU&xT4x7G7Z-9vQ`Xf+3S1#96O4F067x7Vvd4hDRPc{Dx&!hJ>!a(bdhq@@35q2))1 z&B+vYKrHJ^SGTw4em6?@@QwDFPmT+c(+ z#&uME7;rptOQKo1ssDcGFJ|DOOCJ;0+3EX(wrrSt#FI3W<$#?E`Fq4VY7TppW(lND|HVgF9(p z73RwDC>wR3h9^kv$ZR=`FZ&UZYm!TtI%(Ka<(9$xSY>&6=UNFwC{Z!iSqB%0@%Wc| z{Z5F!_>vnD3~xBaMlla$ze?4EU0VGw5f5msmzRCEB*}a_&2072UUkXI(@PGm*2ui6 zOA*LV-?9x4=J2*4^JItc0czlgJlK;i@2z*^0Oih5Y*O!tj=Mj>8Bg|<;o1Y4?kwHu z`Q8kB{N9+8D6sqiLSKxowZFCKfnJGSQhPipuO#@6XYbG6VmA^a*q>6iz5?z<6!c%; zYc)JH0hRpOhQSvP8h!Vd4Uz-gNEo|@%Ti|zJg#3BaNggamR*~tV;7%+ZNNVO!t^gT<`oooAS+`4sqoY3@hhs{@Ys%i> zUpElsaBEh zPuSj04_y7Ay8Y%SxA9s?FepwGpPu!ktI;{BG zce}3jP^_*pF!%k{0LOURu6_)W*{c@KjXf^1WmqUZPD<9+2|1%~b{&s-C0WZ&+ow|dB9U8lNYzxzwUtXQ+ znM-hDX{=ASVRenriWyT)oSI;Np6S`v=4O|iGlC@F-R=~S6Y&8DOIwiNgngRt&#?z- zUYGtokR{Lw1AZ0l#&toZ;>zQ!EC%aOD5Rj@qgF>saIz`0Px3VgGNJ4|1lNYu^ zt@M6F4{5FNFEy_5&M1|(x%ToMEBL}YpAkwP0#hn$X}Gc(pkwFlIyf)sN~w6cxX_l& z5M0#K#0E<2tIHKlU(xAcKl7MThG1=xiuZbgu-4?XReQqctUA9K83^MiAw5i|Rwn!p z%QV?iHMXgv{8K90rN+fb`bD(kN!E(e&G_Yj_;G;p4Lrtve-S#E;>SbTq&p zODz^SpGld@jnsZs39JuU=Q^oA=#~_1SniUA4wE{8sbWf3KZ>X6P;MqX(9$Lq+aPTl z#xixD0or130UKf|>pBN;Bgc(&wOyA#^3g>}rJsCQXjeipAYY>&T7Y@mczU>pvn{V9 z1;B*Mz~tYROcJ-P3I?erOAS_@klD3-!DYR#y{He;MPYx>Hz4^*%N&Ug3>^dA*e{-n zrg(*PDmGGxj@)5mwb1Jhc@|zQBY+4)^Vt*GWBWi-YJgl(VR8pK_&)VYDsH0?cj_Kw z2PseJGW0!gwvX}&6m_HMVohd9j;^^(E1;d7zq`Ta7JA1o{vv|nKi%vUzo9Oc&j z#{&GeoWGs@b&-+NeK61!q$Y9iWI!76>4)|@@mW?ayKoWLo`-C2Hl$XsJy;`l=_PL5 z{ZI{>WJ^Uv!`e8^;+a?&cLLYdQXu+l_k6_x$)wQ&C{CrnkT?gV>|nv*Wf35mCNC z{0Y?D0k|z74W=4P!^;NolI|+Ee#ImUlHhLF)t{CdRC>5gInUvXtV_^;K+#F5^L|sr zX9GHI`v&$+IGTm(r@L$}KU4SEmxoMfIps_)8-#B<9$p2qlM^BM879;@OxH6Glh~c_ z+Bw-J{!~+-l19|E#)9p?h6}ekZu`PXB*;~lL|bC+mu^?aIT1sW^Z<7nW#G+eK_8B30sIUb)R_iy?EcP>rqxEV_YobOZ{7z^JEbiJ2jP&qn(Quyrd8?$89#g`80$+;VB zAMkl?Q55v*6t`ByEIU7mYZ=5kab|b$}Fe)=oZ1*-!YLR6rmqQfMOx4Gzz4W>|*_y!*=m{IP6}f4^ zg1D^L8PKyZ!TwAg+pQ5NbmcaI2DS=^7+DJwzBQMHH-%DImZ6l2@6*tnTMr?EYatg+ z(DiS*@czk*Jpkw!AMOM;j+IsQmuTn`Ef7}&u0->^L$UD9*n4&8NEtz2*@Lo>Dj{}f z^0_B!AKVHGG#p91OTz#M07)$%6QmO)X?QSAr(uhiF7-+urigDewWOE=YqV8U!CSdE z43p38vx^knjt&#W{pr^OQ(Sh@XEi73LH1>%6w* zZhi91&W^iX(zOJ!SQ8&v0v*~-={sj=UK#frME5tLj%Tvf(wBy7{Db$)LETxM&=}l~ z;R{l34$CV!M?8ru4u`9;_)_@}(Pgi_R0R81c;m~nt+x?lpiIS;^Kz+WileLRZe!8< zNt#Xa9cl2^UMTtYUQYiQ4>}hZFh3NWxcEaxuc+lH>_)lH1=~~?E zMrZ#`-}NNX8Jbw1DWvKgh+Gk=ZF|2zjC{Ts$ix@EH*)iv1DaaMNan*bpleGlrw}GXWoPPlOd3R z2_FM`d4m&EHx5f5*zswW?_Rlf<90IZXWQV8LtkCBc6RHoiqfOA&vN9rTXW^fAm!0> zd+b>i9{0=HX=X^62v=L zExcF6x%ts0t@URqGl#2(Uaf*}?ZtR*ti#I#^zH*T&O=Er*=MjvL-!3oFHempt5+)s zdac6!CqGsNNe$(&n_5p7igP z4V7F!S#vpZyM9?{!{IT~0=?w)pL(0{8o&e?rxI7$=^d_2=OzZ1ezFVKfWgNlIF^T< z53L_BEO!v(;)l2K^7l^Zk65kjgveAf``A6wvmh%)oY=*)@@Kl9-F^6iC%3k0CY9Ym zFq*zmVK8^gU22f+YMT6NvBTr05d+667lPM=_#mWe6)aH&db$?!hXuzDUG}}mBrpDM z3s(7wPQ!fHGW18oWhztJ#qqH#eCr}SrYM}ngF}|^^q;o_*s+Zab4awb-Pdnk72yst zK2n0P08+4v!U2SFG0v6&(*}UfMXGkaQ=aO32-)Eb@5H_vGn~g{_wDM99hCQZBEGKi2$Vwl zR@e*A;=*86|dKJ4Pd zhW4d5$5J1rN)L1fA+c9hu~-v}3|uoD%F)a7?DIuE90y_FzA{U$Yh~Qnl1ExI&b)|V zB;N@pMsB4CC>gI_&1$NKzH3om-*Ve=j7XABumI6O+29q%wBJ{0{wGxC-UrP+UKCj= ziIq#rfBAx~Q=nq_SHFy*mD`?}oWw8KQITR$56Kxi0m?GHPscZ8XU5|N1|@d%tn^@! zV)nJ;F^oBTAj$*^X?Qx#M2<1m?-es8AHl72zD*9TqWIrPAakM!VP^$yaOJ_Uy`OPm z$MZT&p}GjWDA*}*-uh`<6cA}-#ExH6jP9V1QM1J_Th{G=A4OWE&i{oysSuwvBE|pj zPZ&S_<3MjnJGx!~mvX^BnLPmNn{z(o@2jyMKr`mRkPdRlBZm{tA8G$nh<^%BQde7< zg^G)%8!`hq5^wKPog*7(UnfG%*~0o)FG_3i+g4Fm7v}Xqy?d9LuOO>{cb_@{C6<7>$<6)Fvuzi2HeDN5S}4nfqUMkjOgsmDzIH8;Xn%u_!})g;BnP63@! z(jqW0&>-1o%`2naKUid~&F$lVuS`I_wF}5vCXC(zI=67A{o+yQ@z`Nwo{=fS{+t}m z9D~t!?_k3rOpUgqWgD;|e=4w z;pGaY;|92S;3_@{Xh$Y3(`*z?J;3;eMqSkl*VoSj87P#@_JEhP%!5Nd3F{Wykd@b=!-Kt1+WindXbBlUL3i7E z8FOlSu79|d*cg~d1Y+RO&RUl^=n25cc>=NJ-caM87gRb9EPCIf(YJcyBHkLQt?hgf zf;M+v^EMmNYrSntD%)1uflG8$rd3{@C@3gkLP0|PMMJcwJsR#^iB30^;^&wOIO`nb zQg-FAL^1kuGl1ajfEfrnoJ3t!$^>W7Rxk+{ zYU!+n zUKj^j4HSp5iSY{7AOTy~{~|@kThAyN>|LPdYg!Wx` z;R9lxw8whx!op>ljzF5~LQq^>zLElj4Pg^dOx(RUTJ7sQ-<@7U_A3~jn^izVM{UVD zdvJB+T6J$5p%?#ct%CkX`9KeJpSd?O*WujOg$-c>I$Czsy_$+Fc;4@-ea8r#$SGMN zp;59HS2|r~E%c&r-BSrv7q>f-aNCzSoJI>Q=s7sXAV6%`lCT;s4g;mY}yIJI4=(oB(6?{BhrkKVbTNy{NKvIOj?fvSi>xnS)* zJYGhe(d$HL$^tQ!>>U6ql+B-|%UA-L`ymzf`ZK75T=>0_O3xCSRdUt7@gH8+RdCpc z6`zj_kGtQ)ZOQOWkbfmKmnWzycg5uTmgBwXO+dhYQ~f|7B)R~Fev*cf>00O-rQdA} zwPC=w2=S$VlV0z94)nDI8mop7H~Q?E=qqA&iL@6Tlxy?HatwXvN6SmLUJxzvy|SHi~dCHJowgAmIR#vN5m9V1^sWYY=`M|85Rype|H+K9RzV{^(JLQ z5rSym+R}2&Gnct6g0%GBy(4}OsmqYXL<@#Do+>+FVBKZmg&07x0&Vw~V(ig7LQl29 z4F!$>%;CReT-v#3jGy&QKG5MXG5`RwR*4nNQC07$!F6km2coKVcUq(}k~b1~&{6EhqczhKL)$OFt8wW2rM- z6GXEPE}DY)J*N>zu3Fx7At@SJ5k-i`+1HQas`u-GCirB;g>4VGUrvc^)L37A^wD)} z8iB9!?)xNndT+SeyH98e2o~!TKJY)y9KW`yC5;k+z=R(}GH{QmoWN03Z~@tH%76we zn>87-h|T&0uKE^#xLE?NQ;)9!HQr<$eiNZFz4)Hf5ZMxH^7m=|^S{aRbKHuc1sHL# zF0YJW5~jo^@~%E^4YVRFI?Iu0B6~1<1Zu%Cf!wzwfQl-ap()TWR0x=SdLeHAtl>8B z^soj*i7Nwg1#4AhGx#*aK)TMVuMHvBT6=;tOtr=zD_K|Xe2X)gy#~GE+8p`ykSe6B zCA4pNc+zfLuT>m^#U>Ct!nU#J&_=*MI)*F{yJ~}5PXOh{O{N$CVo$=R)~1DXgE@Q# z0tLMlR|to=d7xL({G*$kd{v@ z<#UpDzIi5du8m>s|4{Ya@l^kRyr=a|R5VZtsZeIi=2(?VLdFpeNyy&YK}N`GA(dH? zgJZ8_W}PH^99#A{4vxLu*YW-S?&Ds6czhnmImc(d-|yG+^&EGliw?`gF|-%{Hr!4w zgDi<2Q0{8X7}-?bT+I=cIV!z;ssgIYPp^lw zUiA#6xh%~3HqeOW!8`1oPSdn(nm@o$FHDt)903@&+OQx?_|mUSbRtJ47`~aZiG)f> z>f+U3BtIG}b}4rVdTw{{U+cC|6TE3vf#jD40|;d5lWqCalUb(sA1%nDxy`-w)!TjgMU6i%AJtJAT` z1(JgQYx`qGX=M4@4eI&%h~QDcnMMlCSHWE_dg8$Uj2inOuq5t0Yw@0|-;j!f%-so# zU^0+)o;@!een-@z`;Mj^mNsjSBK+EYY(n0`bs^HYf&^^?JR?gz64z60Yp=vm_&vQN zq4Wo?0@9yKJo%bA+BTr!4-Pa9w_8gN99}6lL6G12k$vfr^B-NA&YcnIq<|W{u;Y{P z6~!Bp!`=6_72XwHH@w>$x4U)s;(JMq;lTYz&Rc8OowEi3XVvKrVhk^SZ_oGqX*d%% zcD+*W7oy3-_RICmwF=)N0_o*q$jdtiZ;S7b5q095V)v4~^foVM&E4*Z$J$UpP4`+f z;YzR6+tMUBx zLWoytIvA#94NO3~GI*MbGl6sX#)N~4sOgxCG3P(MbmDw;dwss@-`*IGdv$liG$a`g zJ_G(TxomURK6njS*JImBF)q>ShMX^TulQDQ-jpyj`7CzuX5<`_@6=bVA0WyVHo%t!|;V<||0rYU?mp;Ty zY8a;!p9eT6be?J(s%%=eC?Yr-=#+TFz*9KH;NWl%z_)Fxelz{JEvH_)o4m>i$AKRNhU zyn41?aJL0W`oq1scijD>w2DuJz=Z4Y^ZvUVX_U)vsyf62HLB+>g-g!pM05D;_Ihmw z+-5(qZC$#YMA!J{1Alv{|5?&a4E>RDScH-z$&ljLyE7H%X$IK@%eKw>y5m`4Z3LJG zdq80e|$kc|@jlsi(2@80|lBtkk4DG9rczp*upzS|y9=vw>7XW#waUo_!HpW(P; z{6qfMHu>7@t6l<}&N6GcB#E%P3yY*|zzH{fepB1Xuu!#z!B)AShpl!Vtn~|<{~2Ef z>4*~%?c8JSxu_F2l9{nF)18AZ@ml;mxa3}2c?5&bGyBdR7tCFFO0MrrE2-0ZzNzF_ zTC8cFz5TZQ7bT|$!8e>=xm+sB1|-enZkLEq*|GauhZ*!g4ApDFt+~eI<5GP`%qpua zwz_~lRrONup?R3!W-4C#mZ5})PpaNKS#Z+a>MfnQZs!T-pNBs27iBsa%5qD%Wp)j_ zH69pxt?}j0-1FP?Sv~D9BR;JRM?U%ezT>>?=cuSTo!qzI_y(NQHX&9b8!S^?jO}AV zz&7C%`_1Ytw~T3&#dJj}3(3|)&~D=%`x?1k>sRcf^J0bs-)_L{O1X-+mje2YkQT(d zUQcqha*Q42L?KG^g77OkWYY}0a-6*AzVbVT<$cmj|Eyt7jt7VlTD_nzo$;R1dU$$h zpmt)xcqjIV0sU5JjWlx|=0S(T_VH5@XXy=0fiSo`_(AO^LHm~VEU(w{xm&qMi=VH% zy6(hzKU4B6%w|`8{$0rlJre9|zxC@cotpkp02A_l{9+Yd*WHqmqa6y-xwW71r(#E-Bt15)#-sY_%73TMz$^dX&q=8RPq_p{&|04jcF3$RLS<=_0_G zCou_O&EbD(*~SA+aP;?kMcLz45yaMvHz9)41;kho1R=K#Qu7!-vvE2FugHWq32lEn z>S)WJa>;eG#73YHM<_@Pem6rwX3qzFT=`95;rgT|DX8^_=Ey!UW9yrqf1>eki66-G zpwFjK6Ac-=7sE$f0)}p|-R7jPJ~4MSfZ@>q3yUhVGb)^L?Xc>3KXF`d0#2q-m}Xl= z`O2vcIqAniIhitS88<&9NYQ9cxNO&DbtZ(ZNphHt1n-x$5uu-*POx@C`en5me{55K zB{&|QRXriSE_8(C5)+|O+%8%*?3EctnBYUcx~9n%a+79l^qc$n%p_HQDmjg3H4T*qPr@?1vn&n3fU4 zdrXy-vTD-N%d0nw!Y*WwV2&)Fu)b-PqTJ{a`x#v(+)&4_wBy ziR*UJJ41a=D>eQ{x((~uMj2K|y_5!F28puULA4!L-J3$-*N=ny(VL0?Dq^8x*5^h| zFz@N2_sVA$pZ||~_aB=E@D-q){lBo-mHS`zFf90`YI@Ku8r?B`I2Rg7dq|8N4P}{T z=|(ZOhWcMpT)p%+;x=pL(2A!TT$FIX3R#?BSUl~dV=FLAro)c12>`Dl7_b6dkA zK)U>DrSz=ysSI@oHG!oVlL3Y8u&_a%YJz+E_M_Mw9n&qjz#B|U&#|1Pk$3hep2Op| zBh{J=c~m1fRf4^yrl#Cd(v0du#H$;Wg_Oo*d|Al`$E8bE0K1JEy}KhacVb+!Z8ex# zP}|K7-vO3wac9+B6Ok$0L3aXQ*QyR|^0F%0b&Enpx8On|T2LET9D&ArB}|mYnSs{6HI=Cas>+_u41H@V_eY~_xJ*sI z=Hi;$sku{4&`1E05S}^{ngXGWay_b`LPZb5XB@&VJpd*UU0~p33~j8b^IdpojSRiwBMr{-U!QJrULaEXJZg>EVTSkIdKd zx*h>U-FgyCiiAi(eJkjMuaJB-M4OY9*{O^Vfy|~aVwO~P3y?1|L6Ff4hx{77q^MIh zrC36C(SN^LN4fC9R|7e#*>nlXJmm62kUwZ)j6Z=Y@{*-e* zC2jMx928+akWu{C=M1k@458m-Jbs4?_IoHM4poxg5LI&B{W*<&|GAJ?7O(-qQ z`?A_1`k?k~^;O~_gL+vz}1)3K{}9DU@mx*IU^jVRe0?3j{S}D2T1n$At{nz?W)t z+xR@yUhd16^XwCq{&ev9J@{}{;AvlBQ=9eg<(cj_CVuT1ds%h}dVUH*&P;mgt!5G^ zG1GMNt}1s`_#C3M00yU#Ja+>Kf1?(ylz=y(XN0$enq!iFDu;{h` z29?7JxWRZg`{z98_Mdwja-sH4%*Kh?%VHAt<7sqHa1a2hl{HrCFlZX^-0A1DPz0!g zqrrIb5A#_cI`_=2ivj^h&konMgZ_GIc8kWgr)aLs-4ehz5L+?<&R0NH>p^hs{Y@}J zv&rzGI%P6$fvQ?haPBa8AyqP+P}(jrNsIH`Na^?5eQpj1W!aV40_gRVbnT+ZSiK%s zp~572=tWyJ$G;rd0T)T#EoZ=kcgTaD1_AW!PT@Yuv2MSiEFBL%nG0ju?fQm>Jh+U% z2n-of*9YyJ`h%{M2TFT;@+~JEOLjKm&>wOx$m;yrmp}^;+x(`yw3u=Xs6g`JcIpP% zi~u+|>L1AdPpoC&Qy7}|htq0G(y*ac4(YYC|Fe*JWU)9^GOFi6IaFYE31so)VS!oe zvYHPDqhcUPk z0KnV@#HfkklN{&xsu*;eMyw{ZyiU~2mG8dPsUF$S>O&`fWDL3X_ka%GA!YRxofzFQ zt@mP$mHU=Gb~gv@NxO=IrO%-T%K_+R&7-*-?&+>2`BmSmtZ%8DZh{j>7rQ`}6>d^1o#R)r5`&Fk-y!na);zjRyGalP#;-yxOvfPI38 z?eNRPNrm?&cFs$>WPZ?KX$kg z{dj}UWhSLU5g@7vjD!MV>-zstwFhb!UEa%zK5Nh`5XkO&M`leGdzSksrpJ?w+4|%k z4m`WdUr%YaXJ1dG(U4Zi5?!a1B@G6*XPwDm693?ZKH3L91SV2OagCZjU`*&Qml#ZM ziWEdf5yWG&S8KLs!9_tS)+3IX^Q?xJxoRweAYNkDf%WSZtvvnmPo7q&?HCpA1Oa(t z=^=(pD9voLe#8Ii-F$YyP=O|g;b-41%bsefV}Vloz?u!7X4Z5036<-V=9J5MOm-tD zeFczpJZyc%s4zBp(5nKtX69mILKu(d-2}PanH(>3aCl4u47KHxu;sKRVNPFLTL@?` zMNORcZzYp}khoSDG-xvOp~^CL=Q`JKs(l5?=(_Krle0zCnv`-nc`WTmU|&D54ws5f zfHg`g1K;?at7(&}k8kAa-#f$1SN2w@LsD-Y7!vk$q#vqZb^S(SLnXxI-e(8O>H%^> z_3)L<|E%Ruk@(Q!=Cc2&g_4m@OF2fkG%+8`uW|J}f7o}RLW9lOOOgf#z!BVhb(^6WcL_%bOuXn=Z=hw` zET$aAFbtdb9+vWyvEqZ4veBwKBdj5Kcmsd_`E!pZDOsV{gJZhcK+;FY=?R_LhlZ_s z!Ey)Yc^I8s`Bpz%RhxmZp9ngG>7SUGGY+@83`b=)HgGZ#PQUvyqgd))iw zkW2WRty+erSl>hKG_W?kSTp^TKy4kvBPK`PZzn>Y@8|A@d8`TyCbeT{jw(tB zPy%1{#N(Rp_FZF1BhV0E6leQ9#{83=qzp{C_rH|lkPV~+k4*98bLKAuwLJo>X8@Lk zDTf{ThrjJ|@ulwbmFKb^gh-JuRe-_P&@*N_aXIEyFTSP3%=o`|LcD-gy$6R-jXN*0*#_VF^l%uMyN8C| z+{^~;U%$fgVlAr~S^x=m)^$)j?6V5d!K;@oIdK%#u|j8s=r&(RsUDM<%TCS%Vx5My zuF5k)hM&Z4J_rk-x%=V$Er38BtrEuFo;79}kMKN_e$sa#Hpa!}*+fckp+{pAm}Jb| zXJal7`HZjo7N%m=F?~q-u7xON7DMXYwWlS| z_%Vn?F5#D1wr&dENHx(W_vz}J);HE=-3lJId7ycV_p?qgLj)c2lE+j4!J<1TJ$S#b4J86g#ghug%JFKeM%JjxQ@VKP8V@nmi%lGXEyHh-z#*A5t7b zwennTMPkJ5zTW-w{rmUg<%n<3pBobF8pUv&RiYQLCte0B`Eh8P<_lb!J^Rva{l+u# zbC-rcrp>>-AZW)C!YDfJ{4BCBZ>Kq-sw_%eC)eL(+T`tpZf&LVOa}#WMBww=ux3W> zLcy@%WZHge_AthzKCdQtcXws>);-)p+3H%kN!y=$=g8idbG>C=Fz@F`2-WaDddAm} zL*}m?Yj07OA@&P%!Q&KsO0}RJub>@ifH~!Kb=dt2`$B#Sf2^6I`IB_){)@p?4DsH% zhSmbl-J8ly(VEW6;kVwoxs}CXTS7LzASc93B4-EJrJL-TT&{H*x@DAD59(31vITbt z-^f$X{P9>2i2R;eY%0&J3I2L^pentivP|PT(O&4N472p}?(FYulgk5=(__tlIkY>r zf6!VF*vqVH5G^*apGc*z-1sNEmSW}K7f)Th=kSm-AA*wICt{qpbni|GO%G%Q?K1HN zhTYE<#IOo5_#GLRa~BlGcrD#b)k9DQgO~D+%*;7**?-;jVmVo*5VBOR)X|@6IUg^( zqu8IU@5P&QS3*^Xgw}4|&D{5Eiwg_(4|=MBgZ3JN=e3iVn|>&=mMbEfBSxp0zLfD{ zJRI9K+~UKf!UDd9S?Elzq|2OVYxC4Nbo_n(6v?Fn#i&s_U@DcTm|?vkuC6sg3aD$$ zjfu=6iVH@JirM zh!N+~*sLPCO)g|O=1O{IIG5^oX0O>QSeh@4v?8m-ly;Gd9~qQ$cbkpSo66-Hx@SBY zYFiXe{kqfJ@y!-raiZ}%vORx(`v~E5qhY=wIL#JWKUfyM@!pX4DJk4_Rm{8+jPC5vAuQz@|(jG*V?XMWq7eW zML1oSF*WFpm|)H+Jb*wr*bXrn%DSo08wiwHx$sF<)bO^-<~~v+F0F}ER6dvOuBCSj zhX*uKgp(Dm<+of~3z*9ul}G4o2oDq3Vo%|Vk~89vMS4l(LzJw3iVT~+v?SA@mzzza zgGKayC%31{b6jkVYB=Eoh4JsJDT*`*M6<>ytwA*YSaLwA#gO36?pjq*XH(vs10}0( z{e`)^Wgc&v>u={R`!Yhbd787%gm`a$Z;x{s;o#q`5n*!#_2)EVzxdvUKTC7;>EqN* z@BK8BAtL*zn}6jJgC(dNh(oYI*xNXbz*7$vflxvGi2rZnjPKqDW%kAY@7?h;&qSzi zVik;jsI8`UDw*j}I?$U=9jCo6SPv`8j{`G`dG>?>+`{H_qA?xKPQ9} zwG9m~aj?F)D{~luxbiqO^cb7ZyHWeoXBZnCIqtI~v>BRV-(H7cCuos*dEB3>#5idD z*X_?C5w~e3;a}dFl=~;{$2m7jL5r$@gAVObSCa_~?a-Qcqt7OFhyQ+YxV^^n4dI;7 z`h?V*# z@5*D7e}?y7tMI?g?7a6uTA3%~0V#VQse4|(9eC=`r6w+2%Xjs z%>Tg}8X9WC$IH*Z>@;;Ljk&rv#`;&I)60t&(qKVjp78O2TVr{FpMCuHl6w{1VMM&W zDxyO{Uf#EZC;}&W^k+d@+Dr(-gX7BiRQ?Ufy^eL`%gw{#1RSpqft5h|{vx(EdpnK3 zwpB(sFAoXm9_8#i@J|=?JUzVpmgHpb%0^8OH_}VOlZZM&UrIajRUted;s%}=8HrFq zFH2Y#fQ_sf=#+osLm6~?)l8K$T#}`6Z!k5CC@pZ-Cqq_!7F368hL1HgOx2PeQoRhI zpgwE?UMZcB0Wfan{+?CLu8XQJf_Y!b@@8EARX^Tu3Z&0yyE@L5X8FN9W_^#ke5?NH z1pR%;w(E$BqGw0pUY22Iq#sYb-~mFIo(PoQ&`-_&->Fz^I&M}QK&=VM3nthkb1DB$ zm6t^E4KJEDidi9>8sc~aC3AO)ARmvzV!?T8Gar>L4KP`|lXZ#hC+h0EmlNnp`7aL$ z<^UA8xod7_Ms+L#`=i|fs4phNBO=Y%jZAW{<|ZU2R>ETbIk;;q7j2NN{TiXh zjKrkt7FEuc?@GXI^bhbn3XZXgw}bYI&3&;!1TYNCvJ5sUd@mGcWOBjo(`030H#CDR z21<%{N=#zcQQ#vL*g@nbkJeopSZR)s${iW4Q*-JziduJy;@2^TntWkIlLpnaFD4j* zJ#-u$0sL6nhnV1qPj+5Spbi8wLz8Q(#+2Cs+8=;D<$;{08zMmIR(3Y$lCh((vZTDh zUc;(@s!|_hZJVW}BA!;-Ur_D@0YcYXkn*4)5^SOez%k7fg%+wjKn9+LB_KVZ!MZm0 z36}vVMa1+YB-sCd2BuNz`nV38pN2pEPcZ63W`Z2F%d()~7}|c3{k8-PIN)N?KcQ0c zQ*?^4JJYXlbO!;BT*?~BF&UiIu(7oN^RdOL-;oGMh&Btl9YpRc0@2oPHLRW??;A~u zaP!|~-(4d(K@WLzzAAic>jyuu-g3b7uLr_bFx~Gs6)k~Om0~-u-n!4hZ`@PCp&l;q z72S!6Qtcmsyxu$)*dZ)};1+=Eqm8BQ13{{?5}t}v(B}X{jTS=fJg}N%NkWjxnRo1B0pPhtx-~*3Jfj2 zJaBjh;wGv4)di-t@yFOb{izOsnmRhYJm&+1%~%X13|5@^Gg78Z`-@RjHarL>obB7(-hVsab_ro4yf@}I*Ie~B z^AS?YPl`V$XDRvDjP+0}DWkj*iqx|ilID@lq1`)m$~&!Ag$CK={$k<*T&*>4!Oply zIhB{!#Tj)pbIxmbQ`n^O{APgH+F-uTkepX6PdnDaTu$6lheMb7$S?=od8CY}?vS%A zmVoOFK z0I^tBc<+E(^C@)%z&tBKVm5l9s`uCg*FWko(gGLR5Lh!=<=efxW6VOGI5g=ea`65n z;sW#D{P^jBas76q{NE5%H#~|X=x!kaUBfaBH1VLtM&UFz6(i}aruJ3Zr0!h?I0S0X z8RL`$^h&Bz$Fa`@f%0YrQS~h14uJu#ju}LI5MhrP_Q)Ga;dDg&=VFm;C6KXE_6Qy0 zW1Fk{(tWtv9MkFO7>pvW15VoobuB5v$+Em`Cs+SWbJx;rzq?Cwr|pLiw+qz3aVi8YbiKL(Dl@$2Cz*(FILZ|S6W`?UX&E8e&sYNO7m7}j zf5umLACxw_Kc(G*bdH5xN^>H@T#C(Dm>7W17rE8$?qX#TVyuYL8idj}TpGB1iHAK8 zIcVVN*(95>^)7{?@njIYRGEB05m%rO;Lf=VlBLyc|79vIZAjm+S|fU}(DprhR3vY^ z^nAsE0kR&cqmym5f{iRlJJfP?L5Z#6YtUc(_ntJm!yAEk3W?vL{W}{%92q}xzVn;E z1Cjdh+-<>Bv6S-SYrXY7*TG4xZ;v` z8QvLW-gw?O<|oA*O;L9wL%tHT0Z3i%S_fE62z+1PqB^YcQ(ST6(y>IDB{6YfrPs`? z!6bl3f-hxUmR`}t3B6S&}8zESX%AK za?9VIY)u)i(Q_n*%+S^Y5UCvM9A2Pounv#x5_wXrhd_llJC;D8>WZ*_5jcBaPg8T6 z(GD}|WO;J72^Za)XWg)c3*P@}5;UbOr6i9+W<1ZdS@BAWz>N6a<^3vmq^6ac?RM0a zh@Q4tZ&X#j{JW;hxGfFzP@p8+lLMr;5~wq>{12X%PwCs>B)jr-GeEk8oRg~@=lNBz ze5tiwQZzN2m1k1*dr4y~D2jLSsEl(NCGZtqZf8&k&A)p27DZH4gBue(sLkK(hW!BH zHa)SfFe=4gf_l8eC&z9C4dTcn;S-ySjB8)XQ%_VMpfXXB*5l{TeU}rS^t6coZ!X@gcVHamVCH2AJlyQTM}SEI|AJ-k$YEv@oj_o%*C zuJWY6wC5lCbE7#nD^tY5-)Sw6+B-Mc6W7?q5Z!~fsYzuSm4u8glK&-LGLePOGYS!J zUgt|_X=p)GYR>6sZWdq}sg z1}ySV4O%ZhZLT?iVXtiB+{YRdvlH7=1Iz9BU^?cAsyi>QjZ5mjb+lrsuU6P8&E{;%^Ja-hSWjIESVDj{Q?(dJ7BqT zWjR4$cIeCv*3U=>%7rpZOSOFx7CMzRuQK^Dorc=U-yg^R!Am1VnW;q%BA0dwduBpg zL1X-3{p}{=8X4LkNn9xN{@%23rRW`?xZEt;3I}tI<-+oW(rBmt-Y-J z+%)?n1{V2|PtY6U4LZ-yT=8D~w}7b_YtxWu6Q|(T&kb&a8QJD@bINkn1T^BPP2gel zd+0oUqMhx2rEYAl=3X2TN6vDr#v|Sc@=(hoQEmzYZaT-EdgbAlNJ~3Of2UB9sOE?-HpdZfCo>Ei*~D9JpMmd?K{lSx!Hj2H)W6)C z&+&bBe9~leZtZ}dJ^XD{5H-*B;j1Ba3`v~2(Nw{B6<$RkXfOO*K6%pxdH+`}{`ZOs zVy|xd|7+1AOBns(hxsHU^+dww5v#IL5W?j|G*hoCwN6!uTpz0Tx1zpGD{~_nBanFe zqHRm>X>0j>LKC5^aRRw7KGeJMvMsy;twa;Y$;r8Mf{l)yS+RuwWm)G4ZJm;m5^FZQ zrCN0Hw{N#d*Ui0pGFHx}$BBJAHM-B?DYXKXL7l8jsx(2PV`7k)9{CrhrYR9osI`fA z@2c;ZqM)WK_+C;Xxm?)uL|=c79ZDrd|22ClPYlv^;TufN-vtoWL{$j~PtZl#y|R}o zNJ^^R^;()#^@Z8XA73Ha+pqa|ZQMf(1}*JU?2oK66tti6Q+d z(y-Qniupm8z?jW}epY*LBF4D*T72-4f_|q|dRELxsK{1dO_OcX$nkKK z)V>2(Jk_a{HvWf;R{I3NiGOCxHg#YTH3lWg3HoFWjSgYAr5El){=3;dJ=*k{+9GUc z7$ZjvP_n}6Z$~yCBf{`a+-X@^j?9$ibl{_6I(LAfKEV!v>GE`5?AKjzHXtH*x9q(h z{n#O_rflcUFroWMA3PSEZ9W_|gQyoK1F^d=5np02z7tt?=<{(77N zJ8_#spOoy3A|kP=scDc;l0u+br$8U{4fIogmTfKYS|?X#2wZu=!pPX;Tw0*IsXRx^ z?vvR`A5I`-S@#vT0N>6eO4lw3>_@IJzJ`1;YpQ0$dBByxJ8WkEBT+%Nyw08Vpz3nz z*m(sh26c3JBYLtdgFfc7jmMCvT(6>H{nkgsY3aS^7_H*Q2y;jl)Bw*j&Xt4s9;vEr zY8nK3q=o~AMn>Nc>m3?{SDSEU8$9%3AnX8Xg>*iQ66?Z@PI(@1r|P+~i!GSZCk=Y! zRE{^rSwk6@Q}^C_KEoM;(|&4iQXNT^zKH$=*I!HenyHAXr}KYhSHOainp;UGmV4Hu zZY0!i0x=zmk+TD*WJZ3KZ%p&*WRIGAH7bep&k*u}xv#IU?q5zVZ2^ui0QPDJ7K`00 zP&gHtdi#p#T{h)Kobya(x~}hlYi-oJ;n%xpL!!QPHwcm@U%Erx2EN> z6ap2s3UEVr;+rupffLY^TMFxRMVv z7a2t>`RcpCsPT*$TbRJI4!Q!2WPw~hc9_n^q?Lf?C`?Q1v%;H3D#eI% zGFIm8cIQ|Z9681HtJd9)|6W#UelxZfC*9mKCmryKZG*S?s%a}b9AJ5Il(N4%Xkx_ zt_>jADmvgVNP&9vw`z9h=Tn<;Z&LoIscgb2bJuXK@ZPrqZrnXUVkNWhQrInj`uGdm z4(F}>Jj~t$jyxr#LhN8h`HnlBlaGg6mJrzK6-aOU3Z*?Gc((3N8&*|ySsoey^0yA4 zf=$THxqK(`Inb_zOD(r6OU9EDtDtThVn)D&rw9l_xxR|Z57hD&RI+-Z#1*PV|AgUC z<((BUn|D{CdV)i2TXVVRVngJSK`8G`20a!-sm>4Y_(AvLQ2pa2DM6+4E!9%JlBKQgxOH2WSf zTET4Ji?)*Ei&>A6}v-qXA@D_~eu?CZ6? zOk^9@c&nHFJgDDgBy?a|*Ff*5euCQjimCmwA3yM`u(++x8Uf<6#8b|*#;eJv92$&@ z*W}bE7mY>g%p`rbdZ5wAXak6f6 zX9L2H=0u9w_N!NJUXl*3#4cxzerUqdW_*L2;tz~%IZo0<+wZFk&0X6s9^$#)w%LYI z2-;zn48V3H6F5j4>mB|<4@-|CP6yZ*8(fB|RlM{EUdJ3V>Q?5XJc|XlMILrKEsP$~ zSBMz2k+#tYuDWbukSMCrS|wFg_1wZhhebH8K+4@|licT!VWW}H_-2*d%UpczjakgD ze#8h+Yj)>xWbAP2xr}Z9Cq0ZI#g3^x>bz{-(b12Y zDddW#0$himDa~p=dD78W>xWv<_^ByFymRQ-K9wj>^aUEJuQW?vHc>>h+ROuW~44YyajmFe=){`_4adM zDy?pMr1Iv;y(r}Wh}S3O;GglTr1BPFe`29fp{;UsO!kOz`AEEN&&a5{>XQC-jCK+0 z!E1w+OQX%_l5ZQem=28&hr2W=Y5SxmdIB@uQHsCx9*L#4wvOIP!3O5glh=T4S zc6rL=PK83DULaOs_*OMg=&_E?aO6gF>K1KXkJA}?cH?B!cm0vy)&QGs>_DdDRYDO-h0c~`xkf&}vY;JB+ zQ-+tPg&Ib1U!P`VO-)Cp|1Q8Ao4;~$^oR6`Z_>_Ftr_VMEsM-4sw}XwWSZ+_V=kvJ z?7^P>+rhd6gEmDcAtiW2WRPz)XaFQ*vm-r1@9}c^wKx(GQ4Q7?jP$Oe3W*OXpG6dI z=90S9YfyLe3%uw`5f1xgG%!k5$=yT^S*Un@@)~`8$_0#JM(h{Vz49lOTeh%*!N*bb zpBO>MER?&0o_aK@z{`Jr^33~zyP*VBGx53OkZ8O71w%PMP@GTQ zpYax>=xYp+cm`W$c&3|Tcd)Xt(j4g_++=qaa`7% zl2WK=3EYyosX8UG_K@T<_Zx6f{0j!`G&QO)PK}iSX~&;U-ehD(@S=L4oc%((^{c-l zE&(x__P~9Wyz2aWaEx(Gvn?lCUnvbcYsJ|)>cz06Aiu_O*sg6ZQ19tT!K1g$HA>c8 z+W47$h&P}0q)*SZ3+et_L>ssd<zD=TYRSShSt z>V9^fsZV@LO)Vs8_iJZ|yfA179`KYq-Lc;6QavwZp{rQs~=SI9I-9m z;U$=%o?gg1X1Pl5wdT9Z)?CIo?5HY}Q&oTr>qoNb5zdq!c0*y56Xv2DLp=qC%b6*Q zZ(+piuz%Gk>y`X(qu2L;jo!nF%H{hsjgL7c=64eC)w>LcUprh6-jn>?@vnaS>51k( zv_2^(B94$Dwsg7-tu3q(IetyA=$*@9tyPU{5`KmI+k>LXrA^%sf4HC9&Ff4jk)-?M zTx!Gg>+PJl?b3$0-JtNp^oB_tD>hy>WO)*4#Q+<&+grZfT9vFP-zI1ruw3O?NitWD zK$2xe%XXWcmiKquu)Gy|!fDY@gW|aNi;GKaOFerh zN*!1Q{03}PWfHFYIXT+WGk-bY*dNxGy;gH5RGz+Lci!cU0xqi;U?F%;=5=1DxVz=M z#|7m3&!6jI*q?)0vmNyr)mP%m%p<|l9dPj3Wf)HZ3zA!@~y zg$ql;SMh}mvnSXa<2sL#+lv$jjnAB7mllM*l)JknDmNJtUUgWdB>SL4?H52w9XKg( zqSGZXTHHrpKGf&>6y&iwi$?0{L=1KQM6WK*n%p#gxX6=_cP4pG_2?z>ykM|aE0M65 zY=70vX6;Txo*qh~^x;{10L%ZIlM}$>L}O8CUVdlKhi>0&eqzhCCv6ML^B+}lDHX9uw{3M z-*!nrAifkgo|Clm|4YaCCx^T!H_PnrKPMrsEF(QaYqGUl<5j~v>9SEeRKt#FZbXG& zoO0_QxIQzXVM=3z-(G=%GjrIkdB@J^)R(w#(CGFO9A|>CC0Np&Jbgv25zCyK($8&wH3`%AS9kyrxsIw07BMN}6OK)?x3E;N9491x~2k z$7=#{^Ixu3vese9`5YmNPVv_uJX##{h>x< zeLrW1yAMx7rQKX7=cd)EI`rv+uUyLj+og%@<0^rZ)SkFX1&34|`Jj4bON5E@;n(~2 zY76leOv@r#Dz<`$dP??wo6G!u+EO*Zp+k z3^dn|ou>1QRju9^Bi#wF`6phQvps&A#YgP**}Wa2Jv~zYa;E&0%=O_tVnhr=!_nc+ zK|2Q_&S>@|2}bz(gnyXi=n$v5_wE@7IrVnckis=ToB-{l$er_CeabSY?jF%sF4UtXuIqu+>l$6vS+*IRv!mLR2O|DfxPmvjL z0}#5;fwon2!0rD1N)*1!VE?4W`{gz3hiiT38&gwLx9t;BCOaESrs`~tMtcsnv4^8} zdp9|y3ug_eP6xkM|5OP>ew4{k*zjKu&f}QdlB`FvqGy9E^=rUxleSc?@ zf`V6hm0M)ztcRCoZ||#zO-)NTJ3DuJH;QHg2TZ3Xq9QH ziB-H*Muv&KXVY@t&B0Y=vg=^`z_yNrC$g1Y*&5}cvo0A+DlyT?z>uy8UH(LDIMbZ> zBQ32Ri{(jAOuRT3SNA-z=*YxI?19OL&&r*RjEpq3TQ_F?m4dFsuVPcO@f3KA&-D@8+ z#}eo<+WU{W!e-O>vF`4YsfJbeXi{i*QJI$UCR+hKPfsJ}JQucOM>iL{JDZ-n=lAP% zd)D^!C?KVIiw5Gya!M_FEzR{U55+yK&n8n(nZ?g5+gdmCILjX8+kdF*-eMK~Z2zWd z(8nXnNL;~kduM!u)zW%Xd|}SOT~ETUv@CyF>i3F$d#8QX2D!VysB5r$$MUzcD~`CZ zG&V)(8d&^1)t5gpXHqs7*Rxo5wQVJbzdpBg2~!gl7p&X+Yq>yR(9&oFnTt&7+*i6@ za*Ol5)98zIp6U@H*VE@l~9(GYvcbZxeiQ-O>$VtkRIGNW7Eta(s~uc~%0G@a|uD zSb|e0aqO%)|iY1BF#+&oU&G2;FNjKcit_#QvsE!HL zXk2!5ee&I_MP=|s9oI1j>%5i@%sb7k4y|*EKf360tr)d_toQ(PL z0$EzrWV&8+`bMhqWaJAVQ&BF&Zd@cU`K6wv{YgR2vW*JbT|Qo%TrWla`03o+e2N7E zADv?!n?Knh^sYRJUM$4UQyjI8%j!E76O3ZUh1xWiY$;iUR$};EF)!vOb?%Bkb2(cw zmdy0G`{qzhl)h8w;U!_koe8^4e3V;|2#+WIi@fW7hG_P4a{}55yWxXs+PPL9MV&

Ul{517onu~;lg|#g$)0H2xRc72s zK*~`>N(qj**sf@7Z#QmH>2&0@Dr2g4HZ%UvYqk693@#0j3NJ@P( zes{qRq)f3?4Hq?Nk{-Dhuc4{g0|C|60CPRk_*EqgftDwBjXl6^W#`yg5U)|uGA2H) zzsv}lDeA7M^lbo$3ihJ8fJ?aMs|)u*uNq)+z6c?C(J+(W79Hl>(rSN?hu64UE0#Qt zBg@Hb-0^{Cb@$(&zT_%;>7_Ilw|6GAQw`szyK2Q>^b|KgW%b6LmfV`JV*9cD5jzE} zov(3PtZC<5AmzOqq(D@f1>3*@{$gnQdqj6mf#iez57fsqkS0+uJ#9um8=WC@dcgrh z-9PXA%Z40UKer?X*Tbmr6I}@v;g^DD_|8YW)wl$qSbjWR^ma)vs&Um>NAHOQV|R7M*BMW3m&j37rB&F=C{!{;ndJ~Xm zK2Ed)mbY<3HDL{w=oUgD5~abVv0l5|tM1{}7(dMTI};h{VutfL^&h=IK`AiPVH>du zyevIvNW@be?ma8aemq(b8T2qj>Dv0E-KM^$Oz{3ml_-v!!rs}s{VHp^TV8w!=2am4 z-G?8nI8F}tO4j_S8^i}nvhcjw?T*`yMy1bz>CWa5z1L)E0b^2!gbpLQ`IJugVjXu} zE)dPS3pi*;rqk2;uzG#;QcG`uK~rD0oNm3oqFz+Kvq2&&NN>EIAi6j&Gc+zgDXr~b z8q*7vZ|cY^jWKL)N>44UTm=WA8L0RvHn`p0Md{rfP-s;uTYYr226%cvL?0za<$yK8 z@;i-c^a^Z}He+Y+FsnTdWc++Pk+?|!TG4oi{c;!5ZZt5!=8*ld3{PqRCaeVY<}pBb z$E?FrPfD~Ls5*N0VnuBet>tAmfc{wPR8yZg-bxd^aRQf>;I&Popfq5#%P zdV8r&uZYR^g0kUfA9`nQ@dPiZS}1}dvZDN5AF$Dx~#g|VVj~^JgG$4 zlcsM&h1nlCFfO~h0AFt?fH>0vE?ya5h;-BCrB!Q^_ue2EgSrvD^d?;f`LqHd0c|%h zh5D7+`)LdB{i!(Wb3f(<%JZ*Y$4M)d4FD?EZcykTv8G|OJ;BmO*F7`MH6gsYfA5HQzlb=tk3Y` z9u(ozd4tXV6v~y}mBpXu}>yg`1W-hC>~nb;fw^+D5NM5w8(I>jZ8ZDQRR-eZk!{)#o+G)oajA zY8|?==>Befw<*r1w}}MkW15t2jR|6k&n4rY^j2B^%wr8TN4?aN&j%GVRh|}dc>awv zV>9%_Qq_mF!c*oYAt{4?X8J+KiiuR&0A_PLX2IF@PEp6wjA_7e_Wa*i9tp=A*t-QE zNT(t*lhRyKq^p;M|M?w|x_^2A_sGnvZk{JNT%PJCpsyY2UQ(-EYmDCy%6F;Z+*@eWmA) z>!WU!{w56})IX=U^`EFbdCX|0^az)havr-sLoUnBo>%(Sh4hRFevJ;zq|-)fp%SR$ z8ZxM|jx`D_ovnjwzni}klR|}?N_@KVXfxE=9@T+9WQ6Gq=!u_r;o>~$<24>K%PDT7 zc$EEmYmS5Csu_!<%^SOPEg zUOmrtGU(b400000MKnoVLzE-qmGyY$6y?R#IPfs?IhGUSVL3b=GG-zXRm9^NG_Lis zSjNpFJ>H5$uU0nk5-ZJ4F00000 zU}4B}eM4irb7gy6sBqriZFSfrahbZS0{{R30MNc{ndq`7?94vv000000N{cnpaTE^ z002-h0llpr1YNg>0RR91000*-ptm)0ovG3R00000IIlWvk~jds#i5x_gRVTON_%k5 zICmZK^V6As--_WFa_Q>7 **Existing plugins are not modified by this library.** This is a new foundation for future plugin development. + +--- + +## Table of Contents + +- [Architecture](#architecture) +- [Requirements](#requirements) +- [Building](#building) +- [Integration](#integration) +- [Components](#components) + - [FocusManager](#focusmanager) + - [PluginToolBar](#plugintoolbar) + - [EditableGridWidget](#editablegridwidget) + - [FindReplacePanel](#findreplacepanel) + - [ScopedFindReplacePanel](#scopedfindreplacepanel) +- [Utilities](#utilities) + - [EncodingUtils](#encodingutils) +- [Examples](#examples) +- [Design Decisions](#design-decisions) +- [Directory Layout](#directory-layout) + +--- + +## Architecture + +Each component depends only on `FocusManager`. A plugin that needs just a toolbar doesn't pull in grid or find/replace. `EncodingUtils` is a standalone utility with no component dependencies. + +``` + ┌──────────────────────┐ + │ FocusManager │ + │ (core framework) │ + │ shortcut registry │ + │ optional undo/redo │ + └──────┬───┬───┬───────┘ + │ │ │ + ┌───────────┘ │ └───────────┐ + │ │ │ + ┌─────────▼──┐ ┌────────▼────────┐ ┌───▼──────────────┐ + │PluginToolBar│ │EditableGridWidget│ │ FindReplacePanel │ + │(focus-safe) │ │ (grid + undo) │ │ (base, no scope)│ + └────────────┘ └─────────────────┘ └───────┬──────────┘ + │ extends + ┌───────▼───────────────┐ + │ScopedFindReplacePanel │ + │(adds scope combo box) │ + └───────────────────────┘ + + ┌─────────────┐ + │EncodingUtils │ (standalone utility — no component deps) + └─────────────┘ +``` + +**Namespace:** `QtWlPlugin` + +--- + +## Requirements + +- **CMake** ≥ 3.16 +- **Qt 6** (Core, Gui, Widgets) +- **GLib 2.0** (for encoding conversion via `g_convert_with_fallback`) +- **C++20** compiler +- **Internet connection** on first build (to download enca 1.19) + +The [enca](https://cihar.com/software/enca/) library is downloaded and statically built automatically during the CMake configure step. + +--- + +## Building + +### Standalone + +```bash +cd wlx/wayland_qt_base +mkdir build && cd build +cmake .. +make -j$(nproc) +``` + +This produces `libwayland_qt_base.a` — a static library. Each consumer plugin links it in and produces a self-contained `.wlx`. + +### As part of a plugin build + +In your plugin's `CMakeLists.txt`: + +```cmake +add_subdirectory(../../wayland_qt_base wayland_qt_base) +target_link_libraries(my_plugin PRIVATE wayland_qt_base) +``` + +The public include directory (`include/`) is automatically added, so you can include headers as: + +```cpp +#include +#include +// etc. +``` + +--- + +## Integration + +A minimal plugin using this library: + +```cpp +#include +#include + +class MyPluginWidget : public QWidget { + Q_OBJECT +public: + MyPluginWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *view = new QTextEdit(this); + auto *layout = new QVBoxLayout(this); + + // 1. Create FocusManager (required for all components) + m_fm = new QtWlPlugin::FocusManager(this, view, this); + + // 2. Create a focus-safe toolbar + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + m_toolbar->addToolAction("Save", QKeySequence(Qt::CTRL | Qt::Key_S)); + + // 3. Register plugin-specific shortcuts + m_fm->registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_W), + QtWlPlugin::FocusManager::Always, + [this]() { close(); return true; } + ); + + layout->addWidget(m_toolbar); + layout->addWidget(view); + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; +}; +``` + +--- + +## Components + +### FocusManager + +**Header:** `` + +The core framework that every plugin using this library needs. It solves the fundamental problem of a Qt widget embedded in a non-Qt host application: focus activation, deactivation, bounce prevention, and keyboard shortcut dispatch. + +#### The Problem It Solves + +When a Qt plugin widget is embedded inside Double Commander (a non-Qt application), focus management becomes complex: + +- Clicking inside the plugin must activate it; clicking outside must deactivate it. +- Programmatic focus changes (e.g. tab switching in DC) must not accidentally activate the plugin. +- Newly added child widgets must not steal focus from the host. +- Keyboard shortcuts must only fire when the plugin is active, and some should be suppressed when the user is editing text in an input field. + +FocusManager handles all of this through a single `qApp` event filter. + +#### Constructor + +```cpp +QtWlPlugin::FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent = nullptr); +``` + +- `pluginRoot` — the top-level widget of the plugin (typically `this` in the plugin's main widget). +- `primaryView` — the main content widget that should receive focus by default (e.g. a `QTableView`, `QTextEdit`, `QTreeView`). + +The constructor installs the event filter on `qApp` and sets `pluginRoot`'s focus proxy to `primaryView`. + +#### Activation + +| Method | Description | +|--------|-------------| +| `bool isActive() const` | Returns whether the plugin is currently active. | +| `void setActive(bool)` | Manually activate/deactivate. Emits `activated()` or `deactivated()`. When deactivating, clears focus and returns it to the host. | + +**Signals:** `activated()`, `deactivated()` + +Activation is normally handled automatically by the event filter (click inside → activate, click outside → deactivate), but `setActive()` is available for edge cases. + +#### Input Widget Tracking + +The FocusManager distinguishes between "structural" widgets (buttons, headers, etc.) and "input" widgets (text fields, cell editors) to determine when shortcuts should be suppressed. + +| Method | Description | +|--------|-------------| +| `void addInputWidget(QWidget *w)` | Register a widget as an input widget (e.g. a QLineEdit in a find panel). | +| `void removeInputWidget(QWidget *w)` | Unregister an input widget. | +| `bool isInputWidget(QWidget *w) const` | Returns `true` if `w` is registered or is a descendant of `primaryView` (but not `primaryView` itself — this catches dynamically created cell editors). | +| `QWidget *activeInput() const` | Returns the currently focused input widget, or `nullptr`. | + +**Signals:** `inputWidgetEntered(QWidget *w)`, `inputWidgetExited()` + +#### Focus Proxy + +When showing a secondary panel (e.g. find/replace), the plugin's focus proxy should be redirected so keyboard events reach the panel's input fields. + +| Method | Description | +|--------|-------------| +| `void setFocusProxy(QWidget *proxy)` | Redirect `pluginRoot`'s focus proxy to `proxy`. | +| `void resetFocusProxy()` | Reset focus proxy back to `primaryView`. | +| `void restoreViewFocus()` | Explicitly set focus to `primaryView`. | + +#### Shortcut Registration + +Replaces the hardcoded if-chains in `eventFilter` with a declarative registry. + +```cpp +enum ShortcutContext { WhenNoInput, Always }; +using ShortcutId = int; + +ShortcutId registerShortcut(const QKeySequence &keys, ShortcutContext ctx, + std::function handler); +void unregisterShortcut(ShortcutId id); +``` + +- **`WhenNoInput`** — the shortcut only fires when no input widget has focus. Use for navigation keys, copy/paste, delete, etc. +- **`Always`** — the shortcut fires even when the user is editing text. Use for Ctrl+S (save), Ctrl+F (find), Ctrl+W (close), etc. + +The handler returns `true` to consume the event, `false` to let it propagate. + +#### Optional Undo/Redo + +```cpp +void setUndoStack(QUndoStack *stack); +QUndoStack *undoStack() const; +``` + +When a non-null `QUndoStack` is set, FocusManager automatically registers three shortcuts (all with `WhenNoInput` context): + +| Shortcut | Action | +|----------|--------| +| `Ctrl+Z` | `stack->undo()` | +| `Ctrl+Shift+Z` | `stack->redo()` | +| `Ctrl+Y` | `stack->redo()` | + +Calling `setUndoStack(nullptr)` unregisters them. The consumer retains full access to the stack for pushing custom undo commands. + +#### Saved Focus Widget + +```cpp +void saveFocusWidget(QWidget *w); +``` + +Store a reference to the host application's focus widget so the plugin can restore focus to it when deactivating. This is useful when the host tells the plugin which widget had focus before the plugin was activated. + +#### Event Filter Internals + +The event filter handles five event types: + +1. **`MouseButtonPress`** — geometry-based click detection. Click inside plugin → activate. Click outside → deactivate. +2. **`FocusIn`** — tracks which input widget has focus. Ignores programmatic focus changes (`OtherFocusReason`) when inactive. +3. **`KeyPress`** — iterates the shortcut registry, matches key sequences, checks context, dispatches handlers. +4. **`ChildAdded`** — sets `Qt::NoFocus` on newly created child widgets to prevent focus theft. +5. **`QApplication::focusChanged`** — connected in the constructor. Detects focus leaving the plugin hierarchy. When focus enters the plugin while inactive, bounces it back to the host via `QTimer::singleShot(0, ...)`. + +--- + +### PluginToolBar + +**Header:** `` + +A `QToolBar` subclass that integrates with `FocusManager` to prevent focus issues caused by toolbar interaction. + +#### The Problem It Solves + +In a Qt plugin embedded in a non-Qt host, clicking a toolbar button transfers focus to the button widget. Without intervention, this causes the host to think the plugin lost focus and deactivate it. PluginToolBar prevents this by: + +1. Setting `Qt::NoFocus` on all action widgets (buttons, combo boxes, etc.). +2. Restoring focus to `primaryView` after every action trigger via `QTimer::singleShot`. +3. Applying compact default styling. + +#### API + +```cpp +explicit PluginToolBar(FocusManager *fm, QWidget *parent = nullptr); + +QAction *addToolAction(const QString &text, + const QKeySequence &shortcut = {}, + int ctx = 0 /* FocusManager::WhenNoInput */); +``` + +- `addToolAction` creates a `QAction`, adds it to the toolbar, and optionally registers a keyboard shortcut through `FocusManager`. The shortcut text is appended to the tooltip automatically. +- Any dynamically added widgets are also enforced to `NoFocus` via an `actionEvent` override. + +--- + +### EditableGridWidget + +**Header:** `` + +A `QWidget` wrapping a caller-injected `QTableView` with full editing capabilities, undo/redo support, drag-to-move, and context menus. **Format-agnostic** — it provides no file I/O, no parsing, no encoding detection. + +Because `QTableWidget` is a subclass of `QTableView`, you can inject either: +- A `QTableWidget` for simple item-based workflows +- A plain `QTableView` with any `QAbstractItemModel` (e.g. `QStandardItemModel`, `QSqlTableModel`) for model-based workflows + +All internal data access goes through `QAbstractItemModel` / `QModelIndex` — zero `QTableWidgetItem` dependencies inside the grid. + +#### GridMode + +The constructor requires a `GridMode` enum that determines the undo/memory strategy: + +| Mode | Undo Strategy | Sorting | Best For | +|------|--------------|---------|----------| +| `GridMode::MemoryDocument` | Full `QUndoStack` tracking. Sort stores before/after snapshots in RAM. | Snapshot-based undo | `QTableWidget`, `QStandardItemModel`, in-memory data | +| `GridMode::LiveDatabase` | Bypasses `QUndoStack` for data mutations. Sort issues `model->sort()` (SQL `ORDER BY`) with no RAM snapshot. | Direct `model->sort()` | `QSqlTableModel`, transactional models | + +Both modes share context menus, drag-to-reorder, focus management, keyboard shortcuts, and the `WrapAnywhereDelegate`. + +#### Constructor (Dependency Injection) + +```cpp +// The caller creates and configures the view, then hands it to the grid. +explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); +``` + +**In-memory document (full undo):** +```cpp +auto *tw = new QTableWidget(); +auto *grid = new QtWlPlugin::EditableGridWidget(tw, QtWlPlugin::GridMode::MemoryDocument, fm, this); +``` + +**Live database (transactional):** +```cpp +auto *tv = new QTableView(); +tv->setModel(sqlModel); +auto *grid = new QtWlPlugin::EditableGridWidget(tv, QtWlPlugin::GridMode::LiveDatabase, fm, this); +``` + +#### Features + +| Feature | Details | +|---------|---------| +| **Undo/Redo** | `QUndoStack` registered with FocusManager. In `MemoryDocument` mode: four undo command types (`EditCellCommand`, `RowColCommand`, `DataSnapshotCommand`, `SectionMoveCommand`). In `LiveDatabase` mode: data mutations bypass the undo stack — the transactional model handles reverts. | +| **Cell editing** | Enter to open editor, Escape to cancel (reverts to pre-edit value), Up/Down to navigate between cells while editing, Enter to commit and advance right (wraps to next row). | +| **Arrow navigation** | Left at column 0 wraps to the last column of the previous row. Right at last column wraps to column 0 of the next row. | +| **Copy/Paste** | `copySelection(separator)` copies selected cells via `model()->data()`. `pasteSelection()` inserts clipboard rows — wrapped in undo commands in `MemoryDocument`, direct model calls in `LiveDatabase`. | +| **Insert/Delete** | `insertRows()`, `deleteSelectedRows()`, `insertColumns()`, `deleteSelectedColumns()` — undo-wrapped in `MemoryDocument`, direct `model->insertRows()`/`removeRows()` in `LiveDatabase`. | +| **Drag-to-move** | Multi-select row or column drag via headers. Uses a debounce timer to coalesce Qt's per-section `sectionMoved` signals into a single undo command. | +| **Column sorting** | Click header once to arm, click again to sort. `MemoryDocument`: stores full data snapshot for undo. `LiveDatabase`: issues `model->sort()` (SQL `ORDER BY`) with zero RAM overhead. | +| **Word wrap** | `WrapAnywhereDelegate` enables character-level text wrapping (not just word boundaries). Toggle with `setWordWrap(bool)`. | +| **Context menus** | Right-click on the table or vertical header → row operations. Right-click on horizontal header → column operations. Includes insert from clipboard. | +| **Dirty tracking** | `isDirty()` / `dirtyChanged(bool)` signal based on `QUndoStack::isClean()`. | + +#### API + +```cpp +explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); + +// Access +QTableView *view() const; // Returns the injected view (may be QTableView or QTableWidget) +GridMode mode() const; // Returns the active mode +QUndoStack *undoStack() const; + +// Row operations +void copySelection(char separator = '\t'); +QString getSelectionAsText(char separator = '\t'); +void pasteSelection(); +void pasteSelectionAt(int atRow); +void insertRows(int count, int atRow); +void deleteSelectedRows(); + +// Column operations +void copyColumnSelection(char separator = '\t'); +void pasteColumnSelectionAt(int atCol); +void insertColumns(int count, int atCol); +void deleteSelectedColumns(); + +// Appearance +void setWordWrap(bool wrap); +bool wordWrap() const; +void setShowGrid(bool show); + +// State +bool isDirty() const; + +// Signals +void dirtyChanged(bool dirty); +``` + +The consumer provides a `QTableView` (or `QTableWidget`) with a model already set. All grid operations go through `view()->model()` using `QModelIndex`. + +#### Registered Shortcuts + +These are automatically registered with `FocusManager` (all `WhenNoInput`): + +| Shortcut | Action | +|----------|--------| +| `Ctrl+C` | Copy selection as TSV | +| `Ctrl+V` | Paste from clipboard | +| `Delete` | Delete selected rows | +| `Enter` / `Return` | Edit current cell | +| `↑` `↓` `←` `→` | Navigate with right-wrap | +| `Ctrl+Z` | Undo (via FocusManager) | +| `Ctrl+Shift+Z` / `Ctrl+Y` | Redo (via FocusManager) | + +--- + +### FindReplacePanel + +**Header:** `` + +The **base class** for find & replace UI. Provides inputs, match options, and action buttons but has **no scope concept** — scope is entirely the consumer's responsibility. + +#### UI Layout + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Find: [_______________] Replace: [_______________] │ +│ ☐ Match Case ☐ Match Entire Cell ☐ Regex │ +│ [Find Prev] [Find Next] [Replace] [Replace All] status ✕ │ +└──────────────────────────────────────────────────────────────┘ +``` + +#### API + +```cpp +explicit FindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + +// Configuration +void setReplaceEnabled(bool enabled); // hide replace row for read-only views + +// State +QString findText() const; +QString replaceText() const; +bool matchCase() const; +bool matchEntireCell() const; +bool useRegex() const; + +// Status feedback +void setStatusText(const QString &text); + +// Visibility +void showPanel(bool show); +bool isPanelVisible() const; + +// Signals +void findRequested(bool forward); // true = next, false = previous +void replaceRequested(); +void replaceAllRequested(); +void panelClosed(); +``` + +#### Focus Integration + +- When shown, `setFocusProxy` redirects to the find input and selects all text. +- When hidden, `resetFocusProxy` and `restoreViewFocus` return focus to the primary view. +- Find and replace inputs are registered as input widgets with FocusManager. +- `Ctrl+F` and `Ctrl+R` toggle panel visibility (registered as `Always` shortcuts). + +#### Subclass Extension + +The `optionsRow()` layout is exposed to subclasses for inserting additional widgets: + +```cpp +protected: + QHBoxLayout *optionsRow() const; +``` + +--- + +### ScopedFindReplacePanel + +**Header:** `` + +Extends `FindReplacePanel` with a configurable scope combo box. The consumer provides scope labels and reads the current selection in their signal handlers. + +```cpp +explicit ScopedFindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + +void setScopes(const QStringList &scopes); // e.g. {"All Cells", "Current Column"} +QString currentScope() const; +``` + +#### Usage + +```cpp +auto *panel = new QtWlPlugin::ScopedFindReplacePanel(m_fm, this); +panel->setScopes({"All Cells", "Selected Cells", "Current Column", "Current Row"}); + +connect(panel, &QtWlPlugin::FindReplacePanel::findRequested, + this, [this, panel](bool forward) { + QString scope = panel->currentScope(); + // ... implement search logic using scope + } +); +``` + +--- + +## Utilities + +### EncodingUtils + +**Header:** `` + +Static utility class for encoding detection (via [enca](https://cihar.com/software/enca/)) and encoding conversion (via GLib's `g_convert_with_fallback`). No instance needed — all methods are static. + +#### API + +```cpp +// Detect encoding of raw bytes. Language hint improves accuracy for +// specific scripts (e.g. "ru" for Russian, "zh" for Chinese). +static QString detectEncoding(const QByteArray &data, const QString &language = {}); + +// Detect encoding from a file (reads up to sampleSize bytes). +static QString detectFileEncoding(const QString &filePath, const QString &language = {}, + int sampleSize = 4096, bool readAll = false); + +// Convert from a detected encoding to UTF-8. +static QByteArray toUtf8(const QByteArray &data, const QString &fromEncoding); + +// Convert a UTF-8 QString to a target encoding. +static QByteArray fromUtf8(const QString &text, const QString &toEncoding); + +// One-shot: detect encoding and decode to QString. +static QString decodeToString(const QByteArray &data, const QString &language = {}); + +// Check runtime availability of enca. +static bool isEncaAvailable(); +``` + +#### Usage + +```cpp +// Detect and decode a file +QFile f("/path/to/file.txt"); +f.open(QFile::ReadOnly); +QByteArray raw = f.readAll(); +f.close(); + +QString encoding = QtWlPlugin::EncodingUtils::detectEncoding(raw, "ru"); +// encoding → "windows-1251" + +QString text = QtWlPlugin::EncodingUtils::decodeToString(raw, "ru"); +// text → decoded UTF-8 QString + +// Convert back for saving +QByteArray encoded = QtWlPlugin::EncodingUtils::fromUtf8(text, "windows-1251"); +``` + +#### Supported Encodings + +Detection accuracy depends on the language hint. enca supports: + +| Language | Code | Scripts | +|----------|------|---------| +| Belarusian | `be` | CP1251, ISO-8859-5, IBM866, KOI8-UNI, etc. | +| Bulgarian | `bg` | CP1251, ISO-8859-5, ECMA-113, etc. | +| Chinese | `zh` | BIG5, GBK, GB2312, HZ, etc. | +| Croatian | `hr` | CP1250, ISO-8859-2, ISO-8859-16, etc. | +| Czech | `cs` | CP1250, ISO-8859-2, IBM852, KEYBCS2, etc. | +| Estonian | `et` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Hungarian | `hu` | CP1250, ISO-8859-2, IBM852, etc. | +| Latvian | `lv` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Lithuanian | `lt` | CP1257, ISO-8859-4, ISO-8859-13, etc. | +| Polish | `pl` | CP1250, ISO-8859-2, ISO-8859-16, etc. | +| Russian | `ru` | CP1251, KOI8-R, ISO-8859-5, IBM866, etc. | +| Slovak | `sk` | CP1250, ISO-8859-2, IBM852, KEYBCS2, etc. | +| Slovene | `sl` | CP1250, ISO-8859-2, IBM852, etc. | +| Ukrainian | `uk` | CP1251, KOI8-U, ISO-8859-5, IBM866, etc. | + +Use `"__"` or an empty string to let enca auto-detect from the system locale. + +--- + +## Examples + +### Plugin with toolbar + find/replace (no grid) + +```cpp +#include +#include +#include + +class TextViewerWidget : public QWidget { + Q_OBJECT +public: + TextViewerWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *view = new QTextEdit(this); + auto *layout = new QVBoxLayout(this); + + m_fm = new QtWlPlugin::FocusManager(this, view, this); + + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + m_toolbar->addToolAction("Print", QKeySequence(Qt::CTRL | Qt::Key_P)); + + m_findReplace = new QtWlPlugin::ScopedFindReplacePanel(m_fm, this); + m_findReplace->setScopes({"Entire Document", "Selection"}); + m_findReplace->setReplaceEnabled(false); // read-only viewer + + connect(m_findReplace, &QtWlPlugin::FindReplacePanel::findRequested, + this, &TextViewerWidget::onFind); + + layout->addWidget(m_toolbar); + layout->addWidget(view, 1); + layout->addWidget(m_findReplace); + } + +private slots: + void onFind(bool forward) { + QString query = m_findReplace->findText(); + QString scope = m_findReplace->currentScope(); + // ... implement search + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; + QtWlPlugin::ScopedFindReplacePanel *m_findReplace; +}; +``` + +### Plugin with editable grid + undo (QTableWidget) + +```cpp +#include +#include +#include +#include + +class DataEditorWidget : public QWidget { + Q_OBJECT +public: + DataEditorWidget(QWidget *parent = nullptr) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Inject a QTableWidget for item-based convenience + auto *table = new QTableWidget(); + m_fm = new QtWlPlugin::FocusManager(this, table, this); + m_grid = new QtWlPlugin::EditableGridWidget( + table, QtWlPlugin::GridMode::MemoryDocument, m_fm, this); + + m_toolbar = new QtWlPlugin::PluginToolBar(m_fm, this); + auto *actSave = m_toolbar->addToolAction("Save", + QKeySequence(Qt::CTRL | Qt::Key_S), + QtWlPlugin::FocusManager::Always); + connect(actSave, &QAction::triggered, this, &DataEditorWidget::onSave); + + connect(m_grid, &QtWlPlugin::EditableGridWidget::dirtyChanged, + this, [this](bool dirty) { + setWindowTitle(dirty ? "Data Editor *" : "Data Editor"); + }); + + layout->addWidget(m_toolbar); + layout->addWidget(m_grid, 1); + loadData(); + } + +private: + void loadData() { + // You can still access QTableWidget-specific methods on the original pointer + auto *tw = qobject_cast(m_grid->view()); + tw->setRowCount(100); + tw->setColumnCount(5); + // ... populate cells with QTableWidgetItem + } + + void onSave() { + // ... write data in your format + m_fm->undoStack()->setClean(); + } + + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::PluginToolBar *m_toolbar; + QtWlPlugin::EditableGridWidget *m_grid; +}; +``` + +### Plugin with editable grid + SQL model (QTableView) + +```cpp +#include +#include +#include +#include +#include + +class SqlEditorWidget : public QWidget { + Q_OBJECT +public: + SqlEditorWidget(QSqlDatabase db, QWidget *parent = nullptr) : QWidget(parent) { + auto *layout = new QVBoxLayout(this); + + // Inject a QTableView with a SQL model + auto *sqlView = new QTableView(); + auto *sqlModel = new QSqlTableModel(this, db); + sqlModel->setTable("users"); + sqlModel->select(); + sqlView->setModel(sqlModel); + + m_fm = new QtWlPlugin::FocusManager(this, sqlView, this); + m_grid = new QtWlPlugin::EditableGridWidget( + sqlView, QtWlPlugin::GridMode::LiveDatabase, m_fm, this); + + // The grid handles all focus, context menus, and shortcuts. + // Sorting uses model->sort() → SQL ORDER BY (no RAM snapshot). + // Insert/delete call model directly (no undo stack wrapping). + // model->setData() triggers SQL UPDATEs via QSqlTableModel. + + layout->addWidget(m_grid, 1); + } + +private: + QtWlPlugin::FocusManager *m_fm; + QtWlPlugin::EditableGridWidget *m_grid; +}; +``` + +### Encoding detection on file load + +```cpp +#include + +void MyPlugin::loadFile(const QString &path) { + QFile file(path); + file.open(QFile::ReadOnly); + QByteArray raw = file.readAll(); + file.close(); + + QString encoding = QtWlPlugin::EncodingUtils::detectEncoding(raw); + if (encoding.isEmpty()) + encoding = "UTF-8"; // fallback + + QByteArray utf8 = QtWlPlugin::EncodingUtils::toUtf8(raw, encoding); + QString content = QString::fromUtf8(utf8); + + m_detectedEncoding = encoding; // store for save + // ... display content +} + +void MyPlugin::saveFile(const QString &path) { + QString content = /* ... get content ... */; + QByteArray encoded = QtWlPlugin::EncodingUtils::fromUtf8(content, m_detectedEncoding); + + QFile file(path); + file.open(QFile::WriteOnly); + file.write(encoded); + file.close(); +} +``` + +--- + +## Design Decisions + +### Shortcut Registry over Hardcoded eventFilter + +Existing plugins (`csvview`, `logview`, `kate`) each contain near-identical `eventFilter` implementations with large if-chains mapping key combinations to actions. This library replaces that pattern with a declarative `registerShortcut()` API. New plugins never need to subclass or override `eventFilter` for keyboard shortcuts. + +### Static Library + +Each consumer plugin links `libwayland_qt_base.a` and produces a self-contained `.wlx` with no runtime dependency on a separate shared object. This simplifies deployment — just ship the `.wlx` file. + +### Optional Undo + +Not all plugins need undo/redo (e.g. a read-only log viewer). Calling `setUndoStack()` is optional. When set, undo shortcuts are auto-registered; when cleared, they're auto-unregistered. The consumer always has direct access to the `QUndoStack` for pushing custom commands, checking `isClean()`, etc. + +### FindReplacePanel Hierarchy + +The base class `FindReplacePanel` has zero scope awareness. This is intentional — a text editor plugin's "scope" concept (selection, whole file) is fundamentally different from a grid plugin's (all cells, current column, current row). The base class provides the UI and signals; the consumer implements matching. + +`ScopedFindReplacePanel` adds a scope combo box for plugins that want predefined scope options. It inserts into the base's `optionsRow()` layout, so there's no UI duplication. + +### Model-Based, Format-Agnostic Grid + +`EditableGridWidget` accepts a `QTableView*` via dependency injection and performs all data operations through `QAbstractItemModel` (`model()->data()`, `model()->setData()`, `model()->insertRows()`, etc.). This means the same grid code works identically whether the underlying model is a `QTableWidget`'s internal model, a `QStandardItemModel`, a `QSqlTableModel`, or any custom `QAbstractItemModel`. The consumer is responsible for file I/O, encoding, and format-specific quoting — the grid knows nothing about data formats. + +### GridMode: MemoryDocument vs LiveDatabase + +The `GridMode` enum forces the downstream developer to explicitly acknowledge the memory paradigm of the data they are loading. `MemoryDocument` enables full undo/redo tracking — every edit, insert, delete, and sort is wrapped in a `QUndoCommand` backed by in-memory data snapshots. `LiveDatabase` bypasses all memory-intensive operations: sorting delegates to `model->sort()` (which translates to SQL `ORDER BY` for `QSqlTableModel`), and structural mutations (insert/delete) call the model directly without undo wrappers. This prevents the catastrophic RAM explosion that would occur if a 5-million-row SQLite table were snapshotted into `QVariantList` objects for undo support. Both modes share identical context menus, drag-to-reorder, focus proxying, keyboard shortcuts, and the `WrapAnywhereDelegate`. + +### enca as Build Dependency + +The encoding detection library [enca](https://cihar.com/software/enca/) is downloaded and statically compiled during the CMake configure step. This mirrors the pattern used by `csvview` and avoids requiring enca to be installed system-wide. The static link means no runtime dependency. + +--- + +## Directory Layout + +``` +wlx/wlxbase_wlqt/ +├── CMakeLists.txt +├── README.md +├── include/ +│ └── wlxbase_wlqt/ +│ ├── EditableGridWidget.h +│ ├── EncodingUtils.h +│ ├── FindReplacePanel.h +│ ├── FocusManager.h +│ ├── PluginToolBar.h +│ └── ScopedFindReplacePanel.h +└── src/ + ├── EditableGridWidget.cpp + ├── EncodingUtils.cpp + ├── FindReplacePanel.cpp + ├── FocusManager.cpp + ├── PluginToolBar.cpp + └── ScopedFindReplacePanel.cpp +``` diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h new file mode 100644 index 0000000..8ed40c4 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/CrashLogger.h @@ -0,0 +1,74 @@ +#pragma once + +/// @file CrashLogger.h +/// Global exception logging for WLX plugins. +/// +/// Usage in wlx_entry.cpp: +/// @code +/// #include +/// +/// HWND DCPCALL ListLoad(HWND ParentWin, char* FileToLoad, int ShowFlags) +/// { +/// WLX_TRY { +/// // ... normal plugin code ... +/// } WLX_CATCH("ListLoad"); +/// return nullptr; // fallback return on exception +/// } +/// @endcode +/// +/// Logs are written to .log next to the .wlx file. + +#include +#include + +namespace QtWlPlugin { + +class CrashLogger { +public: + /// Initialize with any address inside the plugin .so/.wlx + /// (typically a function pointer like &ListLoad). + /// Determines the plugin path and creates the log file path. + static void init(void *addressInPlugin); + + /// Log a caught std::exception with stack trace. + static void log(const char *entryPoint, const std::exception &e); + + /// Log an unknown exception (catch ...) with stack trace. + static void logUnknown(const char *entryPoint); + + /// Log a custom message (for warnings, state dumps, etc.) + static void logMessage(const char *entryPoint, const char *message); + + /// Get the log file path (empty if not initialized). + static const std::string &logPath(); + +private: + static void writeEntry(const char *entryPoint, const char *type, + const char *message); +}; + +} // namespace QtWlPlugin + +/// Convenience macros for wrapping WLX entry points. +/// +/// WLX_TRY { ... } WLX_CATCH("FunctionName"); +/// +/// On first use, auto-initializes the logger with the current function address. +#define WLX_TRY \ + { \ + static bool _wlx_logger_init = false; \ + if (!_wlx_logger_init) { \ + _wlx_logger_init = true; \ + QtWlPlugin::CrashLogger::init( \ + reinterpret_cast(&_wlx_logger_init)); \ + } \ + } \ + try + +#define WLX_CATCH(funcName) \ + catch (const std::exception &_wlx_ex) { \ + QtWlPlugin::CrashLogger::log(funcName, _wlx_ex); \ + } \ + catch (...) { \ + QtWlPlugin::CrashLogger::logUnknown(funcName); \ + } diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h new file mode 100644 index 0000000..a5812ec --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EditableGridWidget.h @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +class FocusManager; + +/// Defines the memory and undo strategy for EditableGridWidget. +/// +/// MemoryDocument: Full QUndoStack tracking for all data mutations. +/// Best for QTableWidget, QStandardItemModel, or any in-memory model. +/// Sorting snapshots entire table state for undo. +/// +/// LiveDatabase: Bypasses QUndoStack for data mutations (insert, delete, sort). +/// Best for QSqlTableModel or other transactional models. +/// Sorting delegates to model->sort() (SQL ORDER BY) with no RAM snapshot. +/// Copy, context menus, drag-to-reorder, focus, and shortcuts still work. +enum class GridMode { + MemoryDocument, + LiveDatabase +}; + +/// Custom delegate that wraps text at any character (not just word boundaries). +class WrapAnywhereDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + void setWrapAnywhere(bool wrap); + bool wrapAnywhere() const; + +private: + bool m_wrap = false; +}; + +/// A QTableView wrapper with full undo/redo, keyboard navigation, +/// drag-to-move columns/rows, context menus, and editing support. +/// +/// Accepts a pre-instantiated QTableView* and a GridMode via dependency injection. +/// Because QTableWidget is a subclass of QTableView, you can inject either: +/// +/// // Item-based (in-memory, full undo): +/// auto *grid = new EditableGridWidget(new QTableWidget(), GridMode::MemoryDocument, fm, this); +/// +/// // Database (transactional, no RAM snapshots): +/// auto *sqlView = new QTableView(); +/// sqlView->setModel(sqlModel); +/// auto *grid = new EditableGridWidget(sqlView, GridMode::LiveDatabase, fm, this); +/// +/// All data access goes through QAbstractItemModel — no QTableWidgetItem +/// dependency in the grid's own logic. +class EditableGridWidget : public QWidget { + Q_OBJECT +public: + /// Takes ownership of the view and parents it to this widget. + explicit EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent = nullptr); + + /// Access the underlying view (may be QTableView or QTableWidget). + QTableView *view() const; + GridMode mode() const; + QUndoStack *undoStack() const; + + // --- Data operations (format-agnostic, model-based) --- + void copySelection(char separator = '\t'); + QString getSelectionAsText(char separator = '\t'); + void pasteSelection(); + void pasteSelectionAt(int atRow); + void insertRows(int count, int atRow); + void deleteSelectedRows(); + + void copyColumnSelection(char separator = '\t'); + void pasteColumnSelectionAt(int atCol); + void insertColumns(int count, int atCol); + void deleteSelectedColumns(); + + // --- Appearance --- + void setWordWrap(bool wrap); + bool wordWrap() const; + void setShowGrid(bool show); + + // --- Context menu integration --- + /// Set the filter row widget for Filters toggle in context menu. + void setFilterRow(class FilterRowWidget *filterRow); + + /// Enable the Dark theme toggle in context menu. + void setThemeToggleEnabled(bool enabled); + + /// Register additional context menu entries. + /// The callback receives the QMenu and the clicked QModelIndex. + void setExtraContextMenuCallback( + std::function callback); + + // --- State --- + bool isDirty() const; + +signals: + void dirtyChanged(bool dirty); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void setupView(); + void registerShortcuts(); + void setupDragToMove(); + void showRowContextMenu(const QPoint &pos); + void showColumnContextMenu(const QPoint &pos); + void onSortByColumn(int column); + void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList &roles); + void updateRowNumbers(); + bool isSectionSelected(QHeaderView *header, int logicalIndex) const; + + /// Helper: row/column count via the model + int rowCount() const; + int colCount() const; + + FocusManager *m_fm; + QTableView *m_view; + GridMode m_mode; + QUndoStack *m_undoStack; + WrapAnywhereDelegate *m_wrapDelegate; + bool m_isProgrammaticChange; + + // Sort state + int m_lastSortColumn; + Qt::SortOrder m_lastSortOrder; + + // Drag-to-move state + QHeaderView *m_dragHeader; + int m_dragLogicalIndex; + QList m_dragBeforeOrder; + QSet m_dragSelectedSections; + bool m_isDraggingSection; + QTimer *m_moveDebounceTimer; + + // Context menu integrations + FilterRowWidget *m_filterRow = nullptr; + bool m_themeToggleEnabled = false; + std::function m_extraMenuCallback; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h new file mode 100644 index 0000000..880140a --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/EncodingUtils.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +/// Encoding detection and conversion utility for plugin developers. +/// +/// Wraps the enca library for detection and glib for encoding conversion. +/// All methods are static — no instance needed. +class EncodingUtils { +public: + /// Detect the encoding of raw byte data. + /// @param data Raw bytes to analyze + /// @param language 2-letter ISO language code hint for enca (e.g. "ru", "en", "de"). + /// If empty, derived from the current locale. + /// @return Detected encoding name (e.g. "UTF-8", "windows-1251") or empty on failure. + static QString detectEncoding(const QByteArray &data, const QString &language = {}); + + /// Detect encoding from a file. Reads up to sampleSize bytes by default. + static QString detectFileEncoding(const QString &filePath, const QString &language = {}, + int sampleSize = 4096, bool readAll = false); + + /// Convert byte data from one encoding to UTF-8. + /// Returns the original data if conversion fails. + static QByteArray toUtf8(const QByteArray &data, const QString &fromEncoding); + + /// Convert a UTF-8 QString to a target encoding. + /// Returns UTF-8 bytes if conversion fails. + static QByteArray fromUtf8(const QString &text, const QString &toEncoding); + + /// Detect encoding and decode to QString in one call. + static QString decodeToString(const QByteArray &data, const QString &language = {}); + + /// Check if enca support is available at runtime. + static bool isEncaAvailable(); +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h new file mode 100644 index 0000000..fb59b70 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterRowWidget.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +class QLineEdit; +class QHBoxLayout; +class QTableView; + +namespace QtWlPlugin { + +/// A row of QLineEdit widgets for per-column filtering of a QTableView. +/// +/// Sits between the column headers and the data. Each column gets its own +/// filter input. Widths sync with column widths automatically. +/// +/// Usage: +/// auto *filter = new FilterRowWidget(tableView, this); +/// connect(filter, &FilterRowWidget::filterChanged, ...); +class FilterRowWidget : public QWidget { + Q_OBJECT +public: + explicit FilterRowWidget(QTableView *view, QWidget *parent = nullptr); + + void setFilterVisible(bool visible); + bool isFilterVisible() const; + void clearFilters(); + + /// Rebuild filter inputs when the model/columns change. + void syncToModel(); + +signals: + void filterChanged(int column, const QString &text); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void rebuildInputs(); + void syncWidths(); + + QTableView *m_view; + QHBoxLayout *m_layout; + QVector m_inputs; + bool m_visible = true; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h new file mode 100644 index 0000000..38fb37c --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FilterableHeaderView.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +class QLineEdit; + +namespace QtWlPlugin { + +/// QHeaderView with an optional embedded filter row below the column labels. +/// +/// When filtering is enabled, QLineEdit inputs appear directly under each +/// column header, pixel-aligned with the sections (no spacer hacks needed). +/// +/// Usage: +/// auto *header = new FilterableHeaderView(Qt::Horizontal, tableView); +/// header->setFilterEnabled(true); +/// tableView->setHorizontalHeader(header); +/// connect(header, &FilterableHeaderView::filterChanged, ...); +class FilterableHeaderView : public QHeaderView { + Q_OBJECT +public: + explicit FilterableHeaderView(Qt::Orientation orientation, QWidget *parent = nullptr); + + /// Enable or disable the filter row. + void setFilterEnabled(bool enabled); + bool isFilterEnabled() const; + + /// Enable or disable the header labels (if false, the header collapses to only show the filters). + void setHeaderVisible(bool visible); + bool isHeaderVisible() const; + + /// Clear all filter inputs. + void clearFilters(); + + /// Get the current filter text for a column. + QString filterText(int column) const; + + QSize sizeHint() const override; + +signals: + /// Emitted when the user types in a column's filter input. + void filterChanged(int column, const QString &text); + +protected: + void updateGeometries() override; + void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const override; + +private slots: + void adjustInputPositions(); + +private: + void rebuildInputs(); + + bool m_filterEnabled = false; + bool m_headerVisible = true; + int m_filterRowHeight = 24; + QVector m_inputs; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h new file mode 100644 index 0000000..1f3f487 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FindReplacePanel.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +class QLineEdit; +class QCheckBox; +class QLabel; +class QPushButton; +class QHBoxLayout; +class QVBoxLayout; + +namespace QtWlPlugin { + +class FocusManager; + +/// Base find & replace panel with no scope concept. +/// +/// Provides the UI shell (find/replace inputs, match options, action buttons) +/// and emits signals. The consumer implements the actual matching logic by +/// connecting to findRequested, replaceRequested, and replaceAllRequested. +/// +/// Scope is entirely the consumer's responsibility at this level. +/// For built-in scope support, use ScopedFindReplacePanel instead. +class FindReplacePanel : public QWidget { + Q_OBJECT +public: + explicit FindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + + // --- Configuration --- + void setReplaceEnabled(bool enabled); + + // --- State --- + QString findText() const; + QString replaceText() const; + bool matchCase() const; + bool matchEntireCell() const; + bool useRegex() const; + + // --- Status feedback --- + void setStatusText(const QString &text); + + // --- Visibility --- + void showPanel(bool show); + bool isPanelVisible() const; + + // --- Access for subclasses --- + FocusManager *focusManager() const; + QLabel *statusLabel() const; + +signals: + void findRequested(bool forward); + void replaceRequested(); + void replaceAllRequested(); + void panelClosed(); + +protected: + /// Hook for subclasses to insert widgets into the options row. + QHBoxLayout *optionsRow() const; + +private: + FocusManager *m_fm; + QLineEdit *m_txtFind; + QLineEdit *m_txtReplace; + QLabel *m_lblReplace; + QCheckBox *m_chkMatchCase; + QCheckBox *m_chkMatchEntire; + QCheckBox *m_chkRegex; + QLabel *m_lblStatus; + QHBoxLayout *m_optionsRow; + QPushButton *m_btnReplace; + QPushButton *m_btnReplaceAll; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h new file mode 100644 index 0000000..1bdf3c2 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/FocusManager.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QUndoStack; + +namespace QtWlPlugin { + +/// Core focus management framework for Qt WLX plugins embedded in a host application. +/// +/// Manages the entire focus lifecycle: activation/deactivation on click, +/// focus bounce prevention, input widget tracking, and a shortcut registry +/// that replaces the hardcoded eventFilter if-chains found in existing plugins. +class FocusManager : public QObject { + Q_OBJECT +public: + explicit FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent = nullptr); + ~FocusManager() override; + + // --- Activation --- + bool isActive() const; + void setActive(bool active); + + // --- Input widget tracking --- + void addInputWidget(QWidget *w); + void removeInputWidget(QWidget *w); + bool isInputWidget(QWidget *w) const; + QWidget *activeInput() const; + + // --- Focus proxy management --- + void setFocusProxy(QWidget *proxy); + void resetFocusProxy(); + + // --- Shortcut registration --- + enum ShortcutContext { WhenNoInput, Always }; + using ShortcutId = int; + ShortcutId registerShortcut(const QKeySequence &keys, ShortcutContext ctx, + std::function handler); + void unregisterShortcut(ShortcutId id); + + // --- Optional undo/redo --- + /// When set, auto-registers Ctrl+Z (undo), Ctrl+Shift+Z (redo), Ctrl+Y (redo). + /// The consumer retains full access to the stack for custom manipulation. + void setUndoStack(QUndoStack *stack); + QUndoStack *undoStack() const; + + // --- Saved focus (for restoring to host app) --- + void saveFocusWidget(QWidget *w); + + // --- Access --- + QWidget *pluginRoot() const; + QWidget *primaryView() const; + + // --- Focus restoration --- + void restoreViewFocus(); + static void expectReloadFocus(); + +signals: + void activated(); + void deactivated(); + void inputWidgetEntered(QWidget *w); + void inputWidgetExited(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +private: + void installFocusGuard(); + void restoreFocusToDC(); + + QPointer m_pluginRoot; + QPointer m_primaryView; + bool m_isActive; + QPointer m_savedFocusWidget; + QPointer m_activeInput; + QSet m_extraInputWidgets; + + QUndoStack *m_undoStack; + QVector m_undoShortcutIds; + + struct RegisteredShortcut { + ShortcutId id; + QKeySequence keys; + ShortcutContext ctx; + std::function handler; + }; + QVector m_shortcuts; + ShortcutId m_nextShortcutId; + static bool s_reloadFocusTarget; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h new file mode 100644 index 0000000..504c73a --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginSplitView.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace QtWlPlugin { + +/// Reusable left-panel + right-content splitter. +/// +/// Both structview (tree) and dbview (table list) use this pattern. +/// Sets sensible defaults: left panel 180px, stretch on right. +class PluginSplitView : public QSplitter { + Q_OBJECT +public: + explicit PluginSplitView(QWidget *leftPanel, QWidget *rightContent, + QWidget *parent = nullptr); + + void setLeftWidth(int pixels); + QWidget *leftPanel() const; + QWidget *rightContent() const; + +private: + QWidget *m_left; + QWidget *m_right; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h new file mode 100644 index 0000000..9d460e6 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginStatusBar.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class QHBoxLayout; + +namespace QtWlPlugin { + +/// A compact status bar for WLX plugins. +/// +/// Displays configurable sections separated by vertical lines. +/// Typical content: encoding, format name, row count, extra info. +/// +/// Example: | UTF-8 | JSON | Rows: 4/4 | +/// Example: | Tables: 11 | Views: 0 | SQLite | Rows: 3/59 | +class PluginStatusBar : public QWidget { + Q_OBJECT +public: + explicit PluginStatusBar(QWidget *parent = nullptr); + + void setEncoding(const QString &encoding); + void setFormatInfo(const QString &info); + void setRowCount(int filtered, int total); + void setExtraInfo(const QString &key, const QString &value); + void removeExtraInfo(const QString &key); + +private: + void rebuild(); + QFrame *createSeparator(); + + QLabel *m_encodingLabel; + QLabel *m_formatLabel; + QLabel *m_rowLabel; + QMap m_extras; + QHBoxLayout *m_layout; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h new file mode 100644 index 0000000..6bb3bc3 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/PluginToolBar.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +class FocusManager; + +enum class ButtonDisplay { + IconOnly, + TextOnly, + Both +}; + +enum class IconMode { + System, + Unicode +}; + +/// A QToolBar subclass that automatically integrates with FocusManager. +/// +/// All action widgets are set to Qt::NoFocus, and focus is restored to the +/// primary view after any action trigger. Provides convenience for adding +/// actions with automatic shortcut registration through FocusManager. +class PluginToolBar : public QToolBar { + Q_OBJECT +public: + explicit PluginToolBar(FocusManager *fm, QWidget *parent = nullptr); + + /// Add an action and optionally register a shortcut through FocusManager. + QAction *addToolAction(const QString &text, + const QKeySequence &shortcut = {}, + int ctx = 0 /* FocusManager::WhenNoInput */, + const QString &systemIconName = {}, + const QString &unicodeIcon = {}, + ButtonDisplay display = ButtonDisplay::Both, + IconMode iconMode = IconMode::System); + +protected: + void actionEvent(QActionEvent *event) override; + +private: + void enforceNoFocus(); + FocusManager *m_fm; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h new file mode 100644 index 0000000..72eb137 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ScopedFindReplacePanel.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class QComboBox; + +namespace QtWlPlugin { + +/// FindReplacePanel subclass that adds configurable scope options via a QComboBox. +/// +/// The consumer sets scope labels (e.g. "All Cells", "Current Column") via setScopes(), +/// then reads currentScope() in their signal handlers to filter the search. +class ScopedFindReplacePanel : public FindReplacePanel { + Q_OBJECT +public: + explicit ScopedFindReplacePanel(FocusManager *fm, QWidget *parent = nullptr); + + void setScopes(const QStringList &scopes); + QString currentScope() const; + +private: + QComboBox *m_comboScope; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h new file mode 100644 index 0000000..1e15078 --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/SequentialRowProxyModel.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace QtWlPlugin { + +class SequentialRowProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit SequentialRowProxyModel(QObject *parent = nullptr); + ~SequentialRowProxyModel() override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h new file mode 100644 index 0000000..d2ea90e --- /dev/null +++ b/wlx/wlxbase_wlqt/include/wlxbase_wlqt/ThemeManager.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace QtWlPlugin { + +/// Shared dark/light theme toggle for WLX plugins. +/// +/// Applies a comprehensive Qt stylesheet to the given widget tree. +/// Preference is persisted in QSettings. +class ThemeManager { +public: + enum Theme { Light, Dark }; + + static void applyTheme(QWidget *root, Theme theme); + static Theme currentTheme(); + static void toggleTheme(QWidget *root); + static bool isDark(); + +private: + static QString darkStylesheet(); + static Theme s_current; +}; + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/CrashLogger.cpp b/wlx/wlxbase_wlqt/src/CrashLogger.cpp new file mode 100644 index 0000000..f14665c --- /dev/null +++ b/wlx/wlxbase_wlqt/src/CrashLogger.cpp @@ -0,0 +1,193 @@ +#include + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#else +#include +#endif + +namespace QtWlPlugin { + +static std::string s_logPath; +static std::string s_pluginName; +static std::mutex s_mutex; + +void CrashLogger::init(void *addressInPlugin) +{ + if (!s_logPath.empty()) + return; // already initialized + +#ifndef _WIN32 + Dl_info info; + if (dladdr(addressInPlugin, &info) && info.dli_fname) { + // Plugin path: /path/to/build/myplugin_qt6.wlx + std::string pluginPath(info.dli_fname); + + // Extract directory + char *pathCopy = strdup(pluginPath.c_str()); + std::string dir(dirname(pathCopy)); + free(pathCopy); + + // Extract plugin name (without extension) + char *baseCopy = strdup(pluginPath.c_str()); + std::string base(basename(baseCopy)); + free(baseCopy); + + auto dot = base.rfind('.'); + if (dot != std::string::npos) + base = base.substr(0, dot); + + s_pluginName = base; + s_logPath = dir + "/" + base + ".log"; + } +#else + HMODULE hModule = nullptr; + GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(addressInPlugin), &hModule); + if (hModule) { + char path[MAX_PATH]; + GetModuleFileNameA(hModule, path, MAX_PATH); + std::string pluginPath(path); + + auto lastSlash = pluginPath.rfind('\\'); + std::string dir = (lastSlash != std::string::npos) ? pluginPath.substr(0, lastSlash) : "."; + std::string base = (lastSlash != std::string::npos) ? pluginPath.substr(lastSlash + 1) : pluginPath; + + auto dot = base.rfind('.'); + if (dot != std::string::npos) + base = base.substr(0, dot); + + s_pluginName = base; + s_logPath = dir + "\\" + base + ".log"; + } +#endif +} + +const std::string &CrashLogger::logPath() +{ + return s_logPath; +} + +/// Format a backtrace into the log. +static std::string captureBacktrace() +{ + std::string result; +#ifndef _WIN32 + void *frames[64]; + int count = backtrace(frames, 64); + char **symbols = backtrace_symbols(frames, count); + + if (symbols) { + // Skip first 3 frames (captureBacktrace, writeEntry, log/logUnknown) + for (int i = 3; i < count; ++i) { + result += " "; + + // Try to demangle the symbol + // Format: "./libfoo.so(+0x1234) [0x7fff1234]" + // or: "./libfoo.so(_ZN3Foo3barEv+0x42) [0x7fff1234]" + std::string sym(symbols[i]); + auto lparen = sym.find('('); + auto plus = sym.find('+', lparen != std::string::npos ? lparen : 0); + auto rparen = sym.find(')', lparen != std::string::npos ? lparen : 0); + + if (lparen != std::string::npos && rparen != std::string::npos + && plus != std::string::npos && plus > lparen + 1) { + std::string mangled = sym.substr(lparen + 1, plus - lparen - 1); + int status = -1; + char *demangled = abi::__cxa_demangle(mangled.c_str(), nullptr, nullptr, &status); + if (status == 0 && demangled) { + result += sym.substr(0, lparen + 1); + result += demangled; + result += sym.substr(plus); + free(demangled); + } else { + result += sym; + } + } else { + result += sym; + } + result += "\n"; + } + free(symbols); + } +#endif + return result; +} + +void CrashLogger::writeEntry(const char *entryPoint, const char *type, + const char *message) +{ + if (s_logPath.empty()) + return; + + std::lock_guard lock(s_mutex); + + FILE *f = fopen(s_logPath.c_str(), "a"); + if (!f) + return; + + // Timestamp + time_t now = time(nullptr); + struct tm tm; + localtime_r(&now, &tm); + char timebuf[64]; + strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", &tm); + + fprintf(f, "--- %s [%s] %s ---\n", timebuf, s_pluginName.c_str(), entryPoint); + fprintf(f, " Type: %s\n", type); + if (message && message[0]) + fprintf(f, " Message: %s\n", message); + + // Stack trace + std::string bt = captureBacktrace(); + if (!bt.empty()) { + fprintf(f, " Stack trace:\n%s", bt.c_str()); + } + + fprintf(f, "\n"); + fclose(f); + + // Also print to stderr so it shows in console + fprintf(stderr, "[%s] EXCEPTION in %s: %s: %s\n", + s_pluginName.c_str(), entryPoint, type, + (message && message[0]) ? message : "(no message)"); +} + +void CrashLogger::log(const char *entryPoint, const std::exception &e) +{ + // Try to get the demangled exception type name + const char *typeName = "std::exception"; +#ifndef _WIN32 + int status = -1; + char *demangled = abi::__cxa_demangle(typeid(e).name(), nullptr, nullptr, &status); + if (status == 0 && demangled) { + writeEntry(entryPoint, demangled, e.what()); + free(demangled); + return; + } +#endif + typeName = typeid(e).name(); + writeEntry(entryPoint, typeName, e.what()); +} + +void CrashLogger::logUnknown(const char *entryPoint) +{ + writeEntry(entryPoint, "unknown (catch ...)", "Non-std::exception object thrown"); +} + +void CrashLogger::logMessage(const char *entryPoint, const char *message) +{ + writeEntry(entryPoint, "info", message); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp b/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp new file mode 100644 index 0000000..20bdcd5 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/EditableGridWidget.cpp @@ -0,0 +1,1216 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +// --------------------------------------------------------------------------- +// Undo Commands — all operate through QAbstractItemModel, not QTableWidgetItem +// --------------------------------------------------------------------------- + +class EditCellCommand : public QUndoCommand { +public: + EditCellCommand(QAbstractItemModel *model, const QModelIndex &index, + const QVariant &oldValue, const QVariant &newValue, + QUndoCommand *parent = nullptr) + : QUndoCommand(parent), m_model(model), + m_row(index.row()), m_col(index.column()), + m_oldValue(oldValue), m_newValue(newValue) { + setText(QString("Edit cell (%1, %2)").arg(m_row).arg(m_col)); + } + void undo() override { + QModelIndex idx = m_model->index(m_row, m_col); + m_model->setData(idx, m_oldValue, Qt::EditRole); + } + void redo() override { + QModelIndex idx = m_model->index(m_row, m_col); + m_model->setData(idx, m_newValue, Qt::EditRole); + } +private: + QAbstractItemModel *m_model; + int m_row, m_col; + QVariant m_oldValue, m_newValue; +}; + +class RowColCommand : public QUndoCommand { +public: + RowColCommand(QAbstractItemModel *model, int index, int count, bool isRow, bool isInsert, + QUndoCommand *parent = nullptr) + : QUndoCommand(parent), m_model(model), m_index(index), m_count(count), + m_isRow(isRow), m_isInsert(isInsert) { + setText(QString("%1 %2 %3(s)").arg(isInsert ? "Insert" : "Delete") + .arg(count).arg(isRow ? "row" : "col")); + if (!isInsert) { + // Snapshot data before deletion for undo restore + int crossDim = isRow ? model->columnCount() : model->rowCount(); + for (int i = 0; i < count; ++i) { + QVariantList list; + for (int j = 0; j < crossDim; ++j) { + QModelIndex idx = isRow ? model->index(index + i, j) + : model->index(j, index + i); + list << model->data(idx, Qt::EditRole); + } + m_data << list; + } + } + } + void undo() override { if (m_isInsert) applyDelete(); else applyInsert(); } + void redo() override { if (m_isInsert) applyInsert(); else applyDelete(); } + +private: + void applyInsert() { + if (m_isRow) m_model->insertRows(m_index, m_count); + else m_model->insertColumns(m_index, m_count); + + // Restore saved data if we have any (undo of delete) + if (!m_data.isEmpty()) { + int crossDim = m_isRow ? m_model->columnCount() : m_model->rowCount(); + for (int i = 0; i < m_count && i < m_data.size(); ++i) { + const QVariantList &list = m_data[i]; + for (int j = 0; j < crossDim && j < list.size(); ++j) { + QModelIndex idx = m_isRow ? m_model->index(m_index + i, j) + : m_model->index(j, m_index + i); + m_model->setData(idx, list[j], Qt::EditRole); + } + } + } + } + void applyDelete() { + if (m_isRow) m_model->removeRows(m_index, m_count); + else m_model->removeColumns(m_index, m_count); + } + QAbstractItemModel *m_model; + int m_index, m_count; + QList m_data; + bool m_isRow, m_isInsert; +}; + +class DataSnapshotCommand : public QUndoCommand { +public: + DataSnapshotCommand(QAbstractItemModel *model, const QList &before, + const QList &after, const QString &text) + : m_model(model), m_before(before), m_after(after), m_first(true) { setText(text); } + void undo() override { restore(m_before); } + void redo() override { if (m_first) { m_first = false; return; } restore(m_after); } +private: + void restore(const QList &data) { + for (int r = 0; r < data.size() && r < m_model->rowCount(); ++r) + for (int c = 0; c < data[r].size() && c < m_model->columnCount(); ++c) { + QModelIndex idx = m_model->index(r, c); + m_model->setData(idx, data[r][c], Qt::EditRole); + } + } + QAbstractItemModel *m_model; + QList m_before, m_after; + bool m_first; +}; + +class SectionMoveCommand : public QUndoCommand { +public: + SectionMoveCommand(QHeaderView *header, const QList &beforeOrder, + const QList &afterOrder, const QString &text) + : m_header(header), m_before(beforeOrder), m_after(afterOrder), m_first(true) { setText(text); } + void undo() override { restore(m_before); } + void redo() override { if (m_first) { m_first = false; return; } restore(m_after); } +private: + void restore(const QList &order) { + for (int target = 0; target < order.size(); ++target) { + int logical = order[target]; + int currentVisual = m_header->visualIndex(logical); + if (currentVisual != target) + m_header->moveSection(currentVisual, target); + } + } + QHeaderView *m_header; + QList m_before, m_after; + bool m_first; +}; + +// --------------------------------------------------------------------------- +// WrapAnywhereDelegate — already model-based (uses QModelIndex) +// --------------------------------------------------------------------------- + +void WrapAnywhereDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const { + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + QString text = opt.text; + opt.text.clear(); + QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter); + + if (!text.isEmpty()) { + painter->save(); + QRect textRect = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemText, &opt); + painter->setClipRect(textRect); + painter->setFont(opt.font); + QTextOption textOption; + textOption.setWrapMode(m_wrap ? QTextOption::WrapAnywhere : QTextOption::NoWrap); + textOption.setAlignment(opt.displayAlignment); + if (opt.state & QStyle::State_Selected) + painter->setPen(opt.palette.color(QPalette::HighlightedText)); + else + painter->setPen(opt.palette.color(QPalette::Text)); + painter->drawText(textRect, text, textOption); + painter->restore(); + } +} + +QSize WrapAnywhereDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const { + if (!m_wrap) return QStyledItemDelegate::sizeHint(option, index); + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + QRect textRect = QApplication::style()->subElementRect(QStyle::SE_ItemViewItemText, &opt); + int width = textRect.width(); + if (width <= 0) width = opt.rect.width(); + QTextDocument doc; + doc.setDefaultFont(opt.font); + QTextOption textOption; + textOption.setWrapMode(QTextOption::WrapAnywhere); + doc.setDefaultTextOption(textOption); + doc.setTextWidth(width); + doc.setPlainText(opt.text); + return QSize(width, qMax((int)doc.size().height(), opt.fontMetrics.height())); +} + +void WrapAnywhereDelegate::setWrapAnywhere(bool wrap) { m_wrap = wrap; } +bool WrapAnywhereDelegate::wrapAnywhere() const { return m_wrap; } + +// --------------------------------------------------------------------------- +// EditableGridWidget +// --------------------------------------------------------------------------- + +EditableGridWidget::EditableGridWidget(QTableView *view, GridMode mode, FocusManager *fm, QWidget *parent) + : QWidget(parent) + , m_fm(fm) + , m_view(view) + , m_mode(mode) + , m_isProgrammaticChange(false) + , m_lastSortColumn(-1) + , m_lastSortOrder(Qt::AscendingOrder) + , m_dragHeader(nullptr) + , m_dragLogicalIndex(-1) + , m_isDraggingSection(false) +{ + setFocusPolicy(Qt::NoFocus); + + // Take ownership + m_view->setParent(this); + + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_view); + + setupView(); + + m_undoStack = new QUndoStack(this); + fm->setUndoStack(m_undoStack); + + connect(m_undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) { + emit dirtyChanged(!clean); + }); + connect(m_undoStack, &QUndoStack::indexChanged, this, [this]() { updateRowNumbers(); }); + + // MemoryDocument mode: track data changes for undo integration. + // LiveDatabase mode: the model handles its own transactions; + // intercepting dataChanged would force the entire DB into RAM. + if (m_mode == GridMode::MemoryDocument) { + if (m_view->model()) { + connect(m_view->model(), &QAbstractItemModel::dataChanged, + this, &EditableGridWidget::onDataChanged); + } + + // Stash old value when entering a cell editor + connect(fm, &FocusManager::inputWidgetEntered, this, [this](QWidget *) { + QModelIndex current = m_view->currentIndex(); + if (!current.isValid() || !m_view->model()) return; + + QAbstractItemModel *model = m_view->model(); + QModelIndex sourceIndex = current; + QAbstractItemModel *sourceModel = model; + if (auto *proxy = qobject_cast(model)) { + sourceModel = proxy->sourceModel(); + sourceIndex = proxy->mapToSource(current); + } + + QVariant existing = sourceModel->data(sourceIndex, Qt::UserRole); + if (!existing.isValid()) { + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, + sourceModel->data(sourceIndex, Qt::EditRole), Qt::UserRole); + m_isProgrammaticChange = false; + } + }); + } + + registerShortcuts(); + setupDragToMove(); +} + +void EditableGridWidget::setupView() +{ + m_view->setFocusPolicy(Qt::ClickFocus); + m_view->setSelectionMode(QAbstractItemView::ExtendedSelection); + + m_view->horizontalHeader()->setSectionsClickable(true); + m_view->horizontalHeader()->setHighlightSections(true); + m_view->horizontalHeader()->setSectionsMovable(false); + m_view->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_view->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + m_view->verticalHeader()->setSectionsClickable(true); + m_view->verticalHeader()->setHighlightSections(true); + m_view->verticalHeader()->setSectionsMovable(false); + m_view->verticalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + m_view->verticalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + m_view->setSortingEnabled(false); + + m_wrapDelegate = new WrapAnywhereDelegate(m_view); + m_view->setItemDelegate(m_wrapDelegate); + + m_view->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(m_view, &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showRowContextMenu(pos); }); + connect(m_view->verticalHeader(), &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showRowContextMenu(pos); }); + connect(m_view->horizontalHeader(), &QWidget::customContextMenuRequested, this, + [this](const QPoint &pos) { showColumnContextMenu(pos); }); + connect(m_view->horizontalHeader(), &QHeaderView::sectionClicked, this, + &EditableGridWidget::onSortByColumn); + + // Install event filter on header viewports for drag-to-move + m_view->horizontalHeader()->viewport()->installEventFilter(this); + m_view->verticalHeader()->viewport()->installEventFilter(this); + // Install on view itself for in-editor key handling + m_view->installEventFilter(this); +} + +int EditableGridWidget::rowCount() const +{ + return m_view->model() ? m_view->model()->rowCount() : 0; +} + +int EditableGridWidget::colCount() const +{ + return m_view->model() ? m_view->model()->columnCount() : 0; +} + +void EditableGridWidget::registerShortcuts() +{ + // Ctrl+C → copy + m_fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_C), FocusManager::WhenNoInput, + [this]() { copySelection('\t'); return true; }); + + // Ctrl+V → paste + m_fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_V), FocusManager::WhenNoInput, + [this]() { pasteSelection(); return true; }); + + // Delete → delete selected rows + m_fm->registerShortcut(QKeySequence(Qt::Key_Delete), FocusManager::WhenNoInput, + [this]() { deleteSelectedRows(); return true; }); + + // Enter/Return → edit current cell + m_fm->registerShortcut(QKeySequence(Qt::Key_Return), FocusManager::WhenNoInput, + [this]() { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { m_view->edit(current); return true; } + return false; + }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Enter), FocusManager::WhenNoInput, + [this]() { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { m_view->edit(current); return true; } + return false; + }); + + // Arrow keys with right-wrap + auto arrowHandler = [this](int key) -> bool { + QModelIndex current = m_view->currentIndex(); + if (!current.isValid()) return false; + int visualCol = m_view->horizontalHeader()->visualIndex(current.column()); + int visualRow = m_view->verticalHeader()->visualIndex(current.row()); + int numRows = rowCount(); + int numCols = colCount(); + if (key == Qt::Key_Up) visualRow--; + if (key == Qt::Key_Down) visualRow++; + if (key == Qt::Key_Left) { + visualCol--; + if (visualCol < 0 && visualRow > 0) { + visualCol = numCols - 1; + visualRow--; + } + } + if (key == Qt::Key_Right) { + visualCol++; + if (visualCol >= numCols && visualRow < numRows - 1) { + visualCol = 0; + visualRow++; + } + } + visualRow = qBound(0, visualRow, numRows - 1); + visualCol = qBound(0, visualCol, numCols - 1); + int r = m_view->verticalHeader()->logicalIndex(visualRow); + int c = m_view->horizontalHeader()->logicalIndex(visualCol); + m_view->setCurrentIndex(m_view->model()->index(r, c)); + return true; + }; + + m_fm->registerShortcut(QKeySequence(Qt::Key_Up), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Up); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Down), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Down); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Left), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Left); }); + m_fm->registerShortcut(QKeySequence(Qt::Key_Right), FocusManager::WhenNoInput, + [=]() { return arrowHandler(Qt::Key_Right); }); +} + +void EditableGridWidget::setupDragToMove() +{ + m_moveDebounceTimer = new QTimer(this); + m_moveDebounceTimer->setSingleShot(true); + m_moveDebounceTimer->setInterval(0); + + connect(m_moveDebounceTimer, &QTimer::timeout, this, [this]() { + if (!m_isDraggingSection || !m_dragHeader) return; + + int newVisual = m_dragHeader->visualIndex(m_dragLogicalIndex); + bool anyMoved = (newVisual != m_dragBeforeOrder.indexOf(m_dragLogicalIndex)); + + if (anyMoved) { + bool isHorizontal = (m_dragHeader == m_view->horizontalHeader()); + QList currentOrder; + for (int v = 0; v < m_dragHeader->count(); ++v) + currentOrder.append(m_dragHeader->logicalIndex(v)); + + QList nonSelected; + for (int li : currentOrder) + if (!m_dragSelectedSections.contains(li)) nonSelected.append(li); + + QList selectedInOrder; + for (int li : m_dragBeforeOrder) + if (m_dragSelectedSections.contains(li)) selectedInOrder.append(li); + + int insertIdx = qBound(0, newVisual, nonSelected.size()); + QList targetOrder; + for (int i = 0; i < insertIdx; ++i) targetOrder.append(nonSelected[i]); + for (int li : selectedInOrder) targetOrder.append(li); + for (int i = insertIdx; i < nonSelected.size(); ++i) targetOrder.append(nonSelected[i]); + + for (int v = 0; v < targetOrder.size(); ++v) { + int logical = targetOrder[v]; + int curVisual = m_dragHeader->visualIndex(logical); + if (curVisual != v) m_dragHeader->moveSection(curVisual, v); + } + + QList afterOrder; + for (int v = 0; v < m_dragHeader->count(); ++v) + afterOrder.append(m_dragHeader->logicalIndex(v)); + m_undoStack->push(new SectionMoveCommand(m_dragHeader, m_dragBeforeOrder, afterOrder, + isHorizontal ? "Move columns" : "Move rows")); + updateRowNumbers(); + } + + m_isDraggingSection = false; + m_dragHeader->setSectionsMovable(false); + m_dragHeader = nullptr; + }); + + auto connectMoveDebounce = [this](QHeaderView *header) { + connect(header, &QHeaderView::sectionMoved, this, [this](int, int, int) { + if (m_isDraggingSection) m_moveDebounceTimer->start(); + }); + }; + connectMoveDebounce(m_view->horizontalHeader()); + connectMoveDebounce(m_view->verticalHeader()); +} + +bool EditableGridWidget::eventFilter(QObject *obj, QEvent *event) +{ + QHeaderView *hHeader = m_view->horizontalHeader(); + QHeaderView *vHeader = m_view->verticalHeader(); + + // --- Header viewport: drag-to-move --- + if (event->type() == QEvent::MouseButtonPress) { + if (obj == hHeader->viewport() || obj == vHeader->viewport()) { + QHeaderView *header = (obj == hHeader->viewport()) ? hHeader : vHeader; + auto *me = static_cast(event); + int logicalIndex = header->logicalIndexAt(me->pos()); + if (logicalIndex >= 0 && isSectionSelected(header, logicalIndex)) { + header->setSectionsMovable(true); + m_isDraggingSection = true; + m_dragHeader = header; + m_dragLogicalIndex = logicalIndex; + m_dragBeforeOrder.clear(); + for (int v = 0; v < header->count(); ++v) + m_dragBeforeOrder.append(header->logicalIndex(v)); + + bool isHorizontal = (header == hHeader); + m_dragSelectedSections.clear(); + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + for (const QModelIndex &idx : sel) { + int li = isHorizontal ? idx.column() : idx.row(); + m_dragSelectedSections.insert(li); + } + + QItemSelection savedSel = m_view->selectionModel()->selection(); + QTimer::singleShot(0, this, [this, savedSel]() { + if (m_isDraggingSection) + m_view->selectionModel()->select(savedSel, QItemSelectionModel::ClearAndSelect); + }); + } else { + header->setSectionsMovable(false); + } + } + } + + // --- In-editor key handling --- + if (event->type() == QEvent::KeyPress && obj == m_view && m_fm->activeInput()) { + auto *ke = static_cast(event); + QAbstractItemModel *model = m_view->model(); + if (!model) return QWidget::eventFilter(obj, event); + + if (ke->key() == Qt::Key_Escape) { + QModelIndex current = m_view->currentIndex(); + if (current.isValid()) { + QVariant oldData = model->data(current, Qt::UserRole); + if (oldData.isValid()) { + m_isProgrammaticChange = true; + model->setData(current, oldData, Qt::EditRole); + model->setData(current, QVariant(), Qt::UserRole); + m_isProgrammaticChange = false; + } + } + m_view->closePersistentEditor(m_view->currentIndex()); + return true; + } + + if (ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down) { + QModelIndex current = m_view->currentIndex(); + int r = current.row(), c = current.column(); + if (ke->key() == Qt::Key_Up) r--; + if (ke->key() == Qt::Key_Down) r++; + r = qBound(0, r, rowCount() - 1); + + // Cancel current edit without saving + if (current.isValid()) { + QVariant oldData = model->data(current, Qt::UserRole); + if (oldData.isValid()) { + m_isProgrammaticChange = true; + model->setData(current, oldData, Qt::EditRole); + model->setData(current, QVariant(), Qt::UserRole); + m_isProgrammaticChange = false; + } + } + m_view->closePersistentEditor(m_view->currentIndex()); + QModelIndex target = model->index(r, c); + m_view->setCurrentIndex(target); + m_view->edit(target); + return true; + } + + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + QModelIndex current = m_view->currentIndex(); + int r = current.row(), c = current.column(); + + // Commit via delegate + QAbstractItemDelegate *delegate = m_view->itemDelegateForIndex(current); + QWidget *editor = m_fm->activeInput(); + if (delegate && editor) + delegate->setModelData(editor, model, current); + + // Navigate right (wrap to next row) + int visualCol = m_view->horizontalHeader()->visualIndex(c); + int visualRow = m_view->verticalHeader()->visualIndex(r); + visualCol++; + if (visualCol >= colCount()) { + visualCol = 0; + visualRow++; + } + if (visualRow < rowCount()) { + int nr = m_view->verticalHeader()->logicalIndex(visualRow); + int nc = m_view->horizontalHeader()->logicalIndex(visualCol); + QTimer::singleShot(0, this, [this, nr, nc]() { + if (m_view->model()) + m_view->setCurrentIndex(m_view->model()->index(nr, nc)); + }); + } + return true; + } + } + + return QWidget::eventFilter(obj, event); +} + +QTableView *EditableGridWidget::view() const { return m_view; } +GridMode EditableGridWidget::mode() const { return m_mode; } +QUndoStack *EditableGridWidget::undoStack() const { return m_undoStack; } +bool EditableGridWidget::isDirty() const { return !m_undoStack->isClean(); } + +void EditableGridWidget::setWordWrap(bool wrap) +{ + m_wrapDelegate->setWrapAnywhere(wrap); + m_view->setWordWrap(wrap); + if (wrap) { + m_view->resizeRowsToContents(); + } else { + m_view->verticalHeader()->setDefaultSectionSize(m_view->fontMetrics().height() + 8); + m_view->resizeRowsToContents(); + } +} + +bool EditableGridWidget::wordWrap() const { return m_wrapDelegate->wrapAnywhere(); } +void EditableGridWidget::setShowGrid(bool show) { m_view->setShowGrid(show); } + +void EditableGridWidget::setFilterRow(FilterRowWidget *filterRow) { m_filterRow = filterRow; } +void EditableGridWidget::setThemeToggleEnabled(bool enabled) { m_themeToggleEnabled = enabled; } + +void EditableGridWidget::setExtraContextMenuCallback( + std::function callback) +{ + m_extraMenuCallback = std::move(callback); +} + +void EditableGridWidget::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QList &roles) +{ + Q_UNUSED(bottomRight); + Q_UNUSED(roles); + if (m_isProgrammaticChange || !topLeft.isValid()) return; + + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QModelIndex sourceIndex = topLeft; + QAbstractItemModel *sourceModel = model; + if (auto *proxy = qobject_cast(model)) { + sourceModel = proxy->sourceModel(); + sourceIndex = proxy->mapToSource(topLeft); + } + + QVariant oldData = sourceModel->data(sourceIndex, Qt::UserRole); + if (!oldData.isValid()) return; + + QVariant newValue = sourceModel->data(sourceIndex, Qt::EditRole); + QString oldText = oldData.toString(); + QString newText = newValue.toString(); + + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, QVariant(), Qt::UserRole); // clear stash + m_isProgrammaticChange = false; + + if (oldText != newText) { + m_isProgrammaticChange = true; + sourceModel->setData(sourceIndex, oldData, Qt::EditRole); // revert so undo command applies it + m_isProgrammaticChange = false; + m_undoStack->push(new EditCellCommand(sourceModel, sourceIndex, oldData, newValue)); + } +} + +void EditableGridWidget::onSortByColumn(int column) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + if (column != m_lastSortColumn) { + m_lastSortColumn = column; + m_lastSortOrder = Qt::AscendingOrder; + m_view->horizontalHeader()->setSortIndicator(-1, Qt::AscendingOrder); + return; + } + + // --- LiveDatabase mode --- + // Do not snapshot memory. Just issue the ORDER BY command via model->sort(). + // QSqlTableModel translates this to an SQL query and re-fetches lazily. + if (m_mode == GridMode::LiveDatabase) { + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) { + if (proxy->sourceModel()) + targetModel = proxy->sourceModel(); + } + targetModel->sort(column, m_lastSortOrder); + m_view->horizontalHeader()->setSortIndicatorShown(true); + m_view->horizontalHeader()->setSortIndicator(column, m_lastSortOrder); + m_lastSortOrder = (m_lastSortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; + return; + } + + // --- MemoryDocument mode --- + // Safely snapshot in-memory data for undo/redo. + int rows = rowCount(), cols = colCount(); + + QList beforeData; + for (int r = 0; r < rows; ++r) { + QVariantList row; + for (int c = 0; c < cols; ++c) + row << model->data(model->index(r, c), Qt::EditRole); + beforeData << row; + } + + model->sort(column, m_lastSortOrder); + + QList afterData; + for (int r = 0; r < rows; ++r) { + QVariantList row; + for (int c = 0; c < cols; ++c) + row << model->data(model->index(r, c), Qt::EditRole); + afterData << row; + } + + m_undoStack->push(new DataSnapshotCommand(model, beforeData, afterData, "Sort")); + m_view->horizontalHeader()->setSortIndicatorShown(true); + m_view->horizontalHeader()->setSortIndicator(column, m_lastSortOrder); + m_lastSortOrder = (m_lastSortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; +} + +void EditableGridWidget::updateRowNumbers() +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + QHeaderView *vh = m_view->verticalHeader(); + for (int v = 0; v < rowCount(); ++v) { + int logical = vh->logicalIndex(v); + model->setHeaderData(logical, Qt::Vertical, QString::number(v + 1)); + } +} + +bool EditableGridWidget::isSectionSelected(QHeaderView *header, int logicalIndex) const +{ + QItemSelectionModel *sel = m_view->selectionModel(); + QAbstractItemModel *model = m_view->model(); + if (!sel || !model) return false; + bool isHorizontal = (header == m_view->horizontalHeader()); + if (isHorizontal) { + for (int r = 0; r < rowCount(); ++r) + if (!sel->isSelected(model->index(r, logicalIndex))) return false; + return rowCount() > 0; + } else { + for (int c = 0; c < colCount(); ++c) + if (!sel->isSelected(model->index(logicalIndex, c))) return false; + return colCount() > 0; + } +} + +// --------------------------------------------------------------------------- +// Copy / Paste / Insert / Delete — all via QAbstractItemModel +// --------------------------------------------------------------------------- + +void EditableGridWidget::copySelection(char separator) +{ + QString text = getSelectionAsText(separator); + if (!text.isEmpty()) + QApplication::clipboard()->setText(text); +} + +QString EditableGridWidget::getSelectionAsText(char separator) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return {}; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return {}; + + int minVRow = rowCount(), maxVRow = -1; + int minVCol = colCount(), maxVCol = -1; + for (const auto &index : sel) { + int vr = m_view->verticalHeader()->visualIndex(index.row()); + int vc = m_view->horizontalHeader()->visualIndex(index.column()); + if (vr < minVRow) minVRow = vr; if (vr > maxVRow) maxVRow = vr; + if (vc < minVCol) minVCol = vc; if (vc > maxVCol) maxVCol = vc; + } + + QString outText; + for (int vr = minVRow; vr <= maxVRow; ++vr) { + int r = m_view->verticalHeader()->logicalIndex(vr); + QStringList rowItems; + for (int vc = minVCol; vc <= maxVCol; ++vc) { + int c = m_view->horizontalHeader()->logicalIndex(vc); + QModelIndex idx = model->index(r, c); + QString cellText; + if (m_view->selectionModel()->isSelected(idx)) + cellText = model->data(idx, Qt::DisplayRole).toString(); + rowItems << cellText; + } + outText += rowItems.join(separator) + "\n"; + } + return outText; +} + +void EditableGridWidget::pasteSelection() +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + if (m_mode == GridMode::LiveDatabase) { + // LiveDatabase: insert at end without undo wrapping + pasteSelectionAt(rowCount()); + return; + } + + // MemoryDocument: insert at selection with undo macro for reorder + int targetVisualRow = rowCount(); + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + int minVRow = rowCount(); + for (const auto &index : sel) { + int vr = m_view->verticalHeader()->visualIndex(index.row()); + if (vr < minVRow) minVRow = vr; + } + targetVisualRow = minVRow; + } + + int endRow = rowCount(); + bool needsMove = (targetVisualRow < endRow); + + QList beforeOrder; + QHeaderView *vh = m_view->verticalHeader(); + if (needsMove) { + for (int v = 0; v < vh->count(); ++v) + beforeOrder.append(vh->logicalIndex(v)); + } + + if (needsMove) m_undoStack->beginMacro("Paste rows"); + pasteSelectionAt(endRow); + + int rowsInserted = rowCount() - endRow; + if (rowsInserted > 0 && needsMove) { + QList midOrder; + for (int v = 0; v < vh->count(); ++v) midOrder.append(vh->logicalIndex(v)); + + for (int i = 0; i < rowsInserted; ++i) { + int logicalRow = endRow + i; + int curVisual = vh->visualIndex(logicalRow); + vh->moveSection(curVisual, targetVisualRow + i); + } + + QList afterOrder; + for (int v = 0; v < vh->count(); ++v) afterOrder.append(vh->logicalIndex(v)); + m_undoStack->push(new SectionMoveCommand(vh, midOrder, afterOrder, "Move pasted rows")); + updateRowNumbers(); + } + if (needsMove) m_undoStack->endMacro(); +} + +void EditableGridWidget::pasteSelectionAt(int atRow) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + int targetCols = colCount(); + if (targetCols <= 0) return; + + QString text = QApplication::clipboard()->text(); + if (text.isEmpty()) return; + + QStringList lines = text.split(QRegularExpression("\r?\n")); + if (!lines.isEmpty() && lines.last().isEmpty()) lines.removeLast(); + if (lines.isEmpty()) return; + + // Detect separator: try tab first, then comma + char sep = '\t'; + QStringList testList = lines.first().split(QLatin1Char('\t')); + if (testList.size() != targetCols) { + QStringList commaTest = lines.first().split(QLatin1Char(',')); + if (commaTest.size() == targetCols) sep = ','; + else if (testList.size() != targetCols) return; + } + + int rowsToInsert = lines.size(); + + QAbstractItemModel *targetModel = model; + int targetRow = atRow; + if (auto *proxy = qobject_cast(model)) { + targetModel = proxy->sourceModel(); + if (atRow < proxy->rowCount()) { + QModelIndex proxyIdx = proxy->index(atRow, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } else { + targetRow = targetModel->rowCount(); + } + } + + if (m_mode == GridMode::LiveDatabase) { + // Direct model insertion, no undo command wrapper + targetModel->insertRows(targetRow, rowsToInsert); + } else { + m_isProgrammaticChange = true; + m_undoStack->push(new RowColCommand(targetModel, targetRow, rowsToInsert, true, true)); + m_isProgrammaticChange = false; + } + + for (int i = 0; i < rowsToInsert; ++i) { + QStringList list = lines.at(i).split(QLatin1Char(sep)); + for (int vc = 0; vc < targetCols; ++vc) { + int c = m_view->horizontalHeader()->logicalIndex(vc); + QString cellText = vc < list.size() ? list.at(vc).trimmed() : ""; + m_isProgrammaticChange = true; + targetModel->setData(targetModel->index(targetRow + i, c), cellText, Qt::EditRole); + m_isProgrammaticChange = false; + } + } +} + +void EditableGridWidget::insertRows(int count, int atRow) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || colCount() <= 0 || count <= 0) return; + + QAbstractItemModel *targetModel = model; + int targetRow = atRow; + if (auto *proxy = qobject_cast(model)) { + targetModel = proxy->sourceModel(); + if (atRow < proxy->rowCount()) { + QModelIndex proxyIdx = proxy->index(atRow, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } else { + targetRow = targetModel->rowCount(); + } + } + + if (m_mode == GridMode::LiveDatabase) { + targetModel->insertRows(targetRow, count); + return; + } + m_undoStack->push(new RowColCommand(targetModel, targetRow, count, true, true)); +} + +void EditableGridWidget::deleteSelectedRows() +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + QAbstractItemModel *targetModel = model; + QSortFilterProxyModel *proxy = qobject_cast(model); + if (proxy) { + targetModel = proxy->sourceModel(); + } + + QSet rowsSet; + for (const auto &index : sel) { + if (proxy) { + rowsSet.insert(proxy->mapToSource(index).row()); + } else { + rowsSet.insert(index.row()); + } + } + QList rowsToDelete = rowsSet.values(); + std::sort(rowsToDelete.begin(), rowsToDelete.end(), std::greater()); + + if (m_mode == GridMode::LiveDatabase) { + for (int r : rowsToDelete) + targetModel->removeRow(r); + return; + } + + m_undoStack->beginMacro("Delete rows"); + for (int r : rowsToDelete) + m_undoStack->push(new RowColCommand(targetModel, r, 1, true, false)); + m_undoStack->endMacro(); +} + +void EditableGridWidget::copyColumnSelection(char separator) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + int minCol = colCount(), maxCol = -1; + for (const auto &index : sel) { + if (index.column() < minCol) minCol = index.column(); + if (index.column() > maxCol) maxCol = index.column(); + } + + QString outText; + for (int r = 0; r < rowCount(); ++r) { + QStringList rowItems; + for (int c = minCol; c <= maxCol; ++c) + rowItems << model->data(model->index(r, c), Qt::DisplayRole).toString(); + outText += rowItems.join(separator) + "\n"; + } + QApplication::clipboard()->setText(outText); +} + +void EditableGridWidget::pasteColumnSelectionAt(int atCol) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + QString text = QApplication::clipboard()->text(); + if (text.isEmpty()) return; + + QStringList lines = text.split(QRegularExpression("\r?\n")); + if (!lines.isEmpty() && lines.last().isEmpty()) lines.removeLast(); + if (lines.isEmpty()) return; + + if (lines.size() != rowCount()) { + QMessageBox::warning(this, "Paste Error", + QString("Clipboard contains %1 rows, but table has %2.") + .arg(lines.size()).arg(rowCount())); + return; + } + + char sep = '\t'; + int colsToInsert = lines.first().split(QLatin1Char('\t')).size(); + if (colsToInsert <= 1) { + sep = ','; + colsToInsert = lines.first().split(QLatin1Char(',')).size(); + } + + if (m_mode == GridMode::LiveDatabase) { + // Direct model insertion, no undo command wrapper + targetModel->insertColumns(atCol, colsToInsert); + } else { + m_isProgrammaticChange = true; + m_undoStack->push(new RowColCommand(targetModel, atCol, colsToInsert, false, true)); + m_isProgrammaticChange = false; + } + + QSortFilterProxyModel *proxy = qobject_cast(model); + for (int r = 0; r < rowCount(); ++r) { + int targetRow = r; + if (proxy) { + QModelIndex proxyIdx = proxy->index(r, 0); + targetRow = proxy->mapToSource(proxyIdx).row(); + } + QStringList list = lines.at(r).split(QLatin1Char(sep)); + for (int c = 0; c < colsToInsert; ++c) { + QString cellText = c < list.size() ? list.at(c).trimmed() : ""; + m_isProgrammaticChange = true; + targetModel->setData(targetModel->index(targetRow, atCol + c), cellText, Qt::EditRole); + m_isProgrammaticChange = false; + } + } +} + +void EditableGridWidget::insertColumns(int count, int atCol) +{ + QAbstractItemModel *model = m_view->model(); + if (!model || rowCount() <= 0 || count <= 0) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + if (m_mode == GridMode::LiveDatabase) { + targetModel->insertColumns(atCol, count); + return; + } + m_undoStack->push(new RowColCommand(targetModel, atCol, count, false, true)); +} + +void EditableGridWidget::deleteSelectedColumns() +{ + QAbstractItemModel *model = m_view->model(); + if (!model || !m_view->selectionModel()) return; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (sel.isEmpty()) return; + + QAbstractItemModel *targetModel = model; + if (auto *proxy = qobject_cast(model)) + targetModel = proxy->sourceModel(); + + QSet colsSet; + for (const auto &index : sel) colsSet.insert(index.column()); + QList colsToDelete = colsSet.values(); + std::sort(colsToDelete.begin(), colsToDelete.end(), std::greater()); + + if (m_mode == GridMode::LiveDatabase) { + for (int c : colsToDelete) + targetModel->removeColumn(c); + return; + } + + m_undoStack->beginMacro("Delete cols"); + for (int c : colsToDelete) + m_undoStack->push(new RowColCommand(targetModel, c, 1, false, false)); + m_undoStack->endMacro(); +} + +// --------------------------------------------------------------------------- +// Context Menus +// --------------------------------------------------------------------------- + +void EditableGridWidget::showRowContextMenu(const QPoint &pos) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QMenu menu(this); + + // --- Copy cell / Copy row --- + QModelIndex clickedIdx = m_view->indexAt(pos); + QAction *actCopyCell = nullptr; + QAction *actCopyRow = nullptr; + + if (clickedIdx.isValid()) { + actCopyCell = menu.addAction(QStringLiteral("Copy cell")); + actCopyRow = menu.addAction(QStringLiteral("Copy row")); + menu.addSeparator(); + } + + // --- Selection-based actions --- + QAction *actCopyTSV = nullptr, *actCopyCSV = nullptr, *actDelete = nullptr; + QAction *actInsertAbove = nullptr, *actInsertBelow = nullptr; + QAction *actPasteAbove = nullptr, *actPasteBelow = nullptr; + + int minRow = rowCount(), maxRow = -1, numRows = 0; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + QSet rows; + for (const auto &index : sel) { + rows.insert(index.row()); + if (index.row() < minRow) minRow = index.row(); + if (index.row() > maxRow) maxRow = index.row(); + } + numRows = rows.size(); + actCopyTSV = menu.addAction("Copy Selection as TSV"); + actCopyCSV = menu.addAction("Copy Selection as CSV"); + menu.addSeparator(); + actDelete = menu.addAction("Delete Selected Rows"); + } else { + int clickedRow = m_view->rowAt(pos.y()); + if (clickedRow >= 0) { minRow = maxRow = clickedRow; numRows = 1; } + } + + if (numRows > 0) { + menu.addSeparator(); + QString rowStr = (numRows == 1) ? "1 row" : QString("%1 rows").arg(numRows); + actInsertAbove = menu.addAction(QString("Insert %1 above").arg(rowStr)); + actInsertBelow = menu.addAction(QString("Insert %1 below").arg(rowStr)); + if (!QApplication::clipboard()->text().isEmpty()) { + menu.addSeparator(); + actPasteAbove = menu.addAction("Insert from Clipboard above"); + actPasteBelow = menu.addAction("Insert from Clipboard below"); + } + } + + // --- Extra callback (KV binary ops, etc.) --- + if (m_extraMenuCallback && clickedIdx.isValid()) { + menu.addSeparator(); + m_extraMenuCallback(&menu, clickedIdx); + } + + // --- Filters and Dark theme toggles --- + menu.addSeparator(); + + QAction *actFilters = nullptr; + if (m_filterRow) { + actFilters = menu.addAction(QStringLiteral("Filters")); + actFilters->setCheckable(true); + actFilters->setChecked(m_filterRow->isFilterVisible()); + } + + QAction *actDarkTheme = nullptr; + if (m_themeToggleEnabled) { + actDarkTheme = menu.addAction(QStringLiteral("Dark theme")); + actDarkTheme->setCheckable(true); + actDarkTheme->setChecked(ThemeManager::isDark()); + } + + QAction *res = menu.exec(m_view->viewport()->mapToGlobal(pos)); + QTimer::singleShot(0, this, [this]() { m_fm->restoreViewFocus(); }); + if (!res) return; + + // Handle results + if (res == actCopyCell && clickedIdx.isValid()) { + QString text = model->data(clickedIdx, Qt::DisplayRole).toString(); + QApplication::clipboard()->setText(text); + } else if (res == actCopyRow && clickedIdx.isValid()) { + int row = clickedIdx.row(); + QStringList cells; + for (int c = 0; c < model->columnCount(); ++c) + cells << model->data(model->index(row, c), Qt::DisplayRole).toString(); + QApplication::clipboard()->setText(cells.join(QChar('\t'))); + } else if (res == actCopyTSV) copySelection('\t'); + else if (res == actCopyCSV) copySelection(','); + else if (res == actDelete) deleteSelectedRows(); + else if (res == actInsertAbove) insertRows(numRows, minRow); + else if (res == actInsertBelow) insertRows(numRows, maxRow + 1); + else if (res == actPasteAbove) pasteSelectionAt(minRow); + else if (res == actPasteBelow) pasteSelectionAt(maxRow + 1); + else if (res == actFilters && m_filterRow) { + m_filterRow->setFilterVisible(!m_filterRow->isFilterVisible()); + } else if (res == actDarkTheme) { + ThemeManager::toggleTheme(window()); + } +} + +void EditableGridWidget::showColumnContextMenu(const QPoint &pos) +{ + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + QMenu menu(this); + QAction *actCopy = nullptr, *actDelete = nullptr; + QAction *actInsertLeft = nullptr, *actInsertRight = nullptr; + QAction *actPasteLeft = nullptr, *actPasteRight = nullptr; + + int minCol = colCount(), maxCol = -1, numCols = 0; + QModelIndexList sel = m_view->selectionModel()->selectedIndexes(); + if (!sel.isEmpty()) { + QSet cols; + for (const auto &index : sel) { + cols.insert(index.column()); + if (index.column() < minCol) minCol = index.column(); + if (index.column() > maxCol) maxCol = index.column(); + } + numCols = cols.size(); + actCopy = menu.addAction("Copy Columns"); + menu.addSeparator(); + actDelete = menu.addAction("Delete Selected Columns"); + } else { + int clickedCol = m_view->columnAt(pos.x()); + if (clickedCol >= 0) { minCol = maxCol = clickedCol; numCols = 1; } + } + + if (numCols > 0) { + menu.addSeparator(); + QString colStr = (numCols == 1) ? "1 col" : QString("%1 cols").arg(numCols); + actInsertLeft = menu.addAction(QString("Insert %1 left").arg(colStr)); + actInsertRight = menu.addAction(QString("Insert %1 right").arg(colStr)); + if (!QApplication::clipboard()->text().isEmpty()) { + menu.addSeparator(); + actPasteLeft = menu.addAction("Insert from Clipboard left"); + actPasteRight = menu.addAction("Insert from Clipboard right"); + } + } + + QAction *res = menu.exec(m_view->horizontalHeader()->viewport()->mapToGlobal(pos)); + QTimer::singleShot(0, this, [this]() { m_fm->restoreViewFocus(); }); + if (!res) return; + + if (res == actCopy) copyColumnSelection('\t'); + else if (res == actDelete) deleteSelectedColumns(); + else if (res == actInsertLeft) insertColumns(numCols, minCol); + else if (res == actInsertRight) insertColumns(numCols, maxCol + 1); + else if (res == actPasteLeft) pasteColumnSelectionAt(minCol); + else if (res == actPasteRight) pasteColumnSelectionAt(maxCol + 1); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/EncodingUtils.cpp b/wlx/wlxbase_wlqt/src/EncodingUtils.cpp new file mode 100644 index 0000000..9523459 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/EncodingUtils.cpp @@ -0,0 +1,122 @@ +#include + +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +QString EncodingUtils::detectEncoding(const QByteArray &data, const QString &language) +{ + if (data.isEmpty()) + return {}; + + QString lang = language; + if (lang.isEmpty()) { + // Derive from current locale without changing it + const char *loc = setlocale(LC_ALL, nullptr); + if (loc && strlen(loc) >= 2) + lang = QString::fromLatin1(loc, 2); + else + lang = "__"; + } + + EncaAnalyser analyser = enca_analyser_alloc(lang.toStdString().c_str()); + if (!analyser) + return {}; + + enca_set_threshold(analyser, 1.38); + enca_set_multibyte(analyser, 1); + enca_set_ambiguity(analyser, 1); + enca_set_garbage_test(analyser, 1); + enca_set_filtering(analyser, 0); + + EncaEncoding encoding = enca_analyse(analyser, + (unsigned char *)data.data(), (size_t)data.size()); + + QString result; + if (encoding.charset > 0 && encoding.charset != 27) { + const char *name = enca_charset_name(encoding.charset, ENCA_NAME_STYLE_ICONV); + if (name) + result = QString::fromLatin1(name); + } + + enca_analyser_free(analyser); + return result; +} + +QString EncodingUtils::detectFileEncoding(const QString &filePath, const QString &language, + int sampleSize, bool readAll) +{ + QFile file(filePath); + if (!file.open(QFile::ReadOnly)) + return {}; + + QByteArray data = readAll ? file.readAll() : file.read(sampleSize); + file.close(); + + return detectEncoding(data, language); +} + +QByteArray EncodingUtils::toUtf8(const QByteArray &data, const QString &fromEncoding) +{ + if (fromEncoding.isEmpty() || fromEncoding.compare("UTF-8", Qt::CaseInsensitive) == 0) + return data; + + gsize len; + gchar *converted = g_convert_with_fallback( + data.data(), data.size(), + "UTF-8", fromEncoding.toStdString().c_str(), + NULL, NULL, &len, NULL); + + if (converted) { + QByteArray result(converted, len); + g_free(converted); + return result; + } + return data; +} + +QByteArray EncodingUtils::fromUtf8(const QString &text, const QString &toEncoding) +{ + if (toEncoding.isEmpty() || toEncoding.compare("UTF-8", Qt::CaseInsensitive) == 0) + return text.toUtf8(); + + QByteArray utf8 = text.toUtf8(); + gsize len; + gchar *converted = g_convert_with_fallback( + utf8.data(), utf8.size(), + toEncoding.toStdString().c_str(), "UTF-8", + NULL, NULL, &len, NULL); + + if (converted) { + QByteArray result(converted, len); + g_free(converted); + return result; + } + return utf8; +} + +QString EncodingUtils::decodeToString(const QByteArray &data, const QString &language) +{ + QString encoding = detectEncoding(data, language); + if (encoding.isEmpty()) + return QString::fromUtf8(data); + + QByteArray utf8 = toUtf8(data, encoding); + return QString::fromUtf8(utf8); +} + +bool EncodingUtils::isEncaAvailable() +{ + EncaAnalyser analyser = enca_analyser_alloc("__"); + if (analyser) { + enca_analyser_free(analyser); + return true; + } + return false; +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp b/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp new file mode 100644 index 0000000..0e61cdc --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FilterRowWidget.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +FilterRowWidget::FilterRowWidget(QTableView *view, QWidget *parent) + : QWidget(parent) + , m_view(view) + , m_layout(new QHBoxLayout(this)) +{ + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + setFixedHeight(24); + + // Track header geometry changes to sync widths + if (m_view->horizontalHeader()) + m_view->horizontalHeader()->installEventFilter(this); + + rebuildInputs(); +} + +void FilterRowWidget::rebuildInputs() +{ + // Clear existing + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + + QAbstractItemModel *model = m_view->model(); + if (!model) return; + + int cols = model->columnCount(); + for (int c = 0; c < cols; ++c) { + auto *input = new QLineEdit(this); + input->setPlaceholderText(QStringLiteral("Filter...")); + input->setMaximumHeight(22); + input->setFrame(true); + input->setClearButtonEnabled(true); + + int col = c; + connect(input, &QLineEdit::textChanged, this, [this, col](const QString &text) { + emit filterChanged(col, text); + }); + + m_layout->addWidget(input); + m_inputs.append(input); + } + + syncWidths(); +} + +void FilterRowWidget::syncWidths() +{ + QHeaderView *header = m_view->horizontalHeader(); + if (!header) return; + + for (int c = 0; c < m_inputs.size(); ++c) { + int w = header->sectionSize(c); + m_inputs[c]->setFixedWidth(w); + } +} + +void FilterRowWidget::setFilterVisible(bool visible) +{ + m_visible = visible; + setVisible(visible); +} + +bool FilterRowWidget::isFilterVisible() const +{ + return m_visible; +} + +void FilterRowWidget::clearFilters() +{ + for (auto *input : m_inputs) + input->clear(); +} + +void FilterRowWidget::syncToModel() +{ + rebuildInputs(); +} + +bool FilterRowWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == m_view->horizontalHeader()) { + if (event->type() == QEvent::Resize + || event->type() == QEvent::LayoutRequest) { + syncWidths(); + } + } + return QWidget::eventFilter(obj, event); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp b/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp new file mode 100644 index 0000000..f41e7c7 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FilterableHeaderView.cpp @@ -0,0 +1,173 @@ +#include + +#include +#include +#include +#include + +namespace QtWlPlugin { + +FilterableHeaderView::FilterableHeaderView(Qt::Orientation orientation, QWidget *parent) + : QHeaderView(orientation, parent) +{ + // Re-position filter inputs whenever sections change + connect(this, &QHeaderView::sectionResized, this, &FilterableHeaderView::adjustInputPositions); + connect(this, &QHeaderView::sectionMoved, this, &FilterableHeaderView::adjustInputPositions); + connect(this, &QHeaderView::geometriesChanged, this, &FilterableHeaderView::adjustInputPositions); +} + +void FilterableHeaderView::setFilterEnabled(bool enabled) +{ + if (m_filterEnabled == enabled) + return; + m_filterEnabled = enabled; + + if (m_filterEnabled) { + rebuildInputs(); + } else { + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + } + + // Force header to recalculate its size + updateGeometries(); + viewport()->update(); +} + +bool FilterableHeaderView::isFilterEnabled() const +{ + return m_filterEnabled; +} + +void FilterableHeaderView::clearFilters() +{ + for (auto *input : m_inputs) + input->clear(); +} + +QString FilterableHeaderView::filterText(int column) const +{ + if (column >= 0 && column < m_inputs.size()) + return m_inputs[column]->text(); + return {}; +} + +void FilterableHeaderView::setHeaderVisible(bool visible) +{ + if (m_headerVisible == visible) + return; + m_headerVisible = visible; + updateGeometries(); + viewport()->update(); +} + +bool FilterableHeaderView::isHeaderVisible() const +{ + return m_headerVisible; +} + +QSize FilterableHeaderView::sizeHint() const +{ + int h = 0; + if (m_headerVisible) { + h += QHeaderView::sizeHint().height(); + } + if (m_filterEnabled) { + h += m_filterRowHeight; + } + return QSize(QHeaderView::sizeHint().width(), h); +} + +void FilterableHeaderView::updateGeometries() +{ + int bottomMargin = 0; + if (m_filterEnabled && m_headerVisible) { + bottomMargin = m_filterRowHeight; + } + setViewportMargins(0, 0, 0, bottomMargin); + + QHeaderView::updateGeometries(); + + if (m_filterEnabled) + adjustInputPositions(); +} + +void FilterableHeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const +{ + if (!m_headerVisible) { + painter->save(); + QStyleOptionHeader opt; + initStyleOption(&opt); + opt.section = logicalIndex; + opt.rect = rect; + opt.text.clear(); + opt.icon = QIcon(); + style()->drawControl(QStyle::CE_HeaderSection, &opt, painter, this); + painter->restore(); + return; + } + QHeaderView::paintSection(painter, rect, logicalIndex); +} + +void FilterableHeaderView::adjustInputPositions() +{ + if (!m_filterEnabled || !model()) + return; + + int cols = model()->columnCount(); + + // Rebuild if column count changed + if (m_inputs.size() != cols) + rebuildInputs(); + + int labelHeight = m_headerVisible ? QHeaderView::sizeHint().height() : 0; + + for (int i = 0; i < m_inputs.size() && i < cols; ++i) { + int logicalIdx = logicalIndex(i); + if (logicalIdx < 0 || logicalIdx >= m_inputs.size()) + continue; + + int xPos = sectionViewportPosition(logicalIdx); + int w = sectionSize(logicalIdx); + + m_inputs[logicalIdx]->setGeometry(xPos, labelHeight, w, m_filterRowHeight); + m_inputs[logicalIdx]->setVisible(!isSectionHidden(logicalIdx)); + } +} + +void FilterableHeaderView::rebuildInputs() +{ + for (auto *input : m_inputs) + delete input; + m_inputs.clear(); + + if (!model() || !m_filterEnabled) + return; + + int cols = model()->columnCount(); + m_inputs.reserve(cols); + + for (int c = 0; c < cols; ++c) { + auto *input = new QLineEdit(this); + input->setPlaceholderText(QStringLiteral("Filter...")); + input->setFrame(true); + input->setClearButtonEnabled(true); + + // Use a small font to fit the compact row height + QFont f = input->font(); + f.setPointSize(f.pointSize() - 1); + input->setFont(f); + + int col = c; + connect(input, &QLineEdit::textChanged, this, [this, col](const QString &text) { + emit filterChanged(col, text); + }); + + m_inputs.append(input); + } + + adjustInputPositions(); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp b/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp new file mode 100644 index 0000000..409b71c --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FindReplacePanel.cpp @@ -0,0 +1,151 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +FindReplacePanel::FindReplacePanel(FocusManager *fm, QWidget *parent) + : QWidget(parent) + , m_fm(fm) +{ + setObjectName("FindReplacePanel"); + setVisible(false); + setStyleSheet( + "QWidget#FindReplacePanel { background-color: palette(window); border-top: 1px solid palette(mid); }" + "QPushButton { border: 1px solid palette(mid); border-radius: 3px; padding: 2px 8px; background-color: palette(button); }" + "QPushButton:hover { background-color: palette(light); }" + "QPushButton:pressed { background-color: palette(midlight); }" + "QPushButton#CloseButton { border: none; background: transparent; }" + "QPushButton#CloseButton:hover { background-color: palette(light); }" + ); + + auto *panelLayout = new QVBoxLayout(this); + panelLayout->setContentsMargins(6, 6, 6, 6); + panelLayout->setSpacing(6); + + // --- Row 1: Find + Replace inputs --- + auto *row1 = new QHBoxLayout(); + row1->setSpacing(6); + + auto *lblFind = new QLabel("Find:", this); + m_txtFind = new QLineEdit(this); + m_txtFind->setPlaceholderText("Search query..."); + + m_lblReplace = new QLabel("Replace:", this); + m_txtReplace = new QLineEdit(this); + m_txtReplace->setPlaceholderText("Replacement text..."); + + row1->addWidget(lblFind); + row1->addWidget(m_txtFind, 1); + row1->addWidget(m_lblReplace); + row1->addWidget(m_txtReplace, 1); + + // --- Row 2: Options + actions --- + m_optionsRow = new QHBoxLayout(); + m_optionsRow->setSpacing(6); + + m_chkMatchCase = new QCheckBox("Match Case", this); + m_chkMatchEntire = new QCheckBox("Match Entire Cell", this); + m_chkRegex = new QCheckBox("Regular Expression", this); + + m_chkMatchCase->setFocusPolicy(Qt::NoFocus); + m_chkMatchEntire->setFocusPolicy(Qt::NoFocus); + m_chkRegex->setFocusPolicy(Qt::NoFocus); + + auto *btnFindPrev = new QPushButton("Find Previous", this); + auto *btnFindNext = new QPushButton("Find Next", this); + m_btnReplace = new QPushButton("Replace", this); + m_btnReplaceAll = new QPushButton("Replace All", this); + + btnFindPrev->setFocusPolicy(Qt::NoFocus); + btnFindNext->setFocusPolicy(Qt::NoFocus); + m_btnReplace->setFocusPolicy(Qt::NoFocus); + m_btnReplaceAll->setFocusPolicy(Qt::NoFocus); + + m_lblStatus = new QLabel(this); + m_lblStatus->setStyleSheet("color: palette(link); font-weight: bold;"); + + auto *btnClose = new QPushButton("\u2715", this); + btnClose->setObjectName("CloseButton"); + btnClose->setFixedWidth(30); + btnClose->setFlat(true); + btnClose->setFocusPolicy(Qt::NoFocus); + + m_optionsRow->addWidget(m_chkMatchCase); + m_optionsRow->addWidget(m_chkMatchEntire); + m_optionsRow->addWidget(m_chkRegex); + m_optionsRow->addWidget(btnFindPrev); + m_optionsRow->addWidget(btnFindNext); + m_optionsRow->addWidget(m_btnReplace); + m_optionsRow->addWidget(m_btnReplaceAll); + m_optionsRow->addWidget(m_lblStatus, 1); + m_optionsRow->addWidget(btnClose); + + panelLayout->addLayout(row1); + panelLayout->addLayout(m_optionsRow); + + // --- Connections --- + connect(btnFindNext, &QPushButton::clicked, this, [this]() { emit findRequested(true); }); + connect(btnFindPrev, &QPushButton::clicked, this, [this]() { emit findRequested(false); }); + connect(m_btnReplace, &QPushButton::clicked, this, &FindReplacePanel::replaceRequested); + connect(m_btnReplaceAll, &QPushButton::clicked, this, &FindReplacePanel::replaceAllRequested); + connect(btnClose, &QPushButton::clicked, this, [this]() { showPanel(false); }); + connect(m_txtFind, &QLineEdit::returnPressed, this, [this]() { emit findRequested(true); }); + connect(m_txtReplace, &QLineEdit::returnPressed, this, &FindReplacePanel::replaceRequested); + + // Register find/replace inputs as input widgets with FocusManager + fm->addInputWidget(m_txtFind); + fm->addInputWidget(m_txtReplace); + + // Register shortcuts (Always context — should work even when editing) + fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_F), FocusManager::Always, + [this]() { showPanel(!isPanelVisible()); return true; }); + fm->registerShortcut(QKeySequence(Qt::CTRL | Qt::Key_R), FocusManager::Always, + [this]() { showPanel(!isPanelVisible()); return true; }); +} + +void FindReplacePanel::setReplaceEnabled(bool enabled) +{ + m_lblReplace->setVisible(enabled); + m_txtReplace->setVisible(enabled); + m_btnReplace->setVisible(enabled); + m_btnReplaceAll->setVisible(enabled); +} + +QString FindReplacePanel::findText() const { return m_txtFind->text(); } +QString FindReplacePanel::replaceText() const { return m_txtReplace->text(); } +bool FindReplacePanel::matchCase() const { return m_chkMatchCase->isChecked(); } +bool FindReplacePanel::matchEntireCell() const { return m_chkMatchEntire->isChecked(); } +bool FindReplacePanel::useRegex() const { return m_chkRegex->isChecked(); } + +void FindReplacePanel::setStatusText(const QString &text) { m_lblStatus->setText(text); } + +void FindReplacePanel::showPanel(bool show) +{ + setVisible(show); + if (show) { + m_fm->setFocusProxy(m_txtFind); + m_txtFind->setFocus(Qt::OtherFocusReason); + m_txtFind->selectAll(); + m_lblStatus->clear(); + } else { + m_fm->resetFocusProxy(); + m_lblStatus->clear(); + m_fm->restoreViewFocus(); + emit panelClosed(); + } +} + +bool FindReplacePanel::isPanelVisible() const { return isVisible(); } + +FocusManager *FindReplacePanel::focusManager() const { return m_fm; } +QLabel *FindReplacePanel::statusLabel() const { return m_lblStatus; } +QHBoxLayout *FindReplacePanel::optionsRow() const { return m_optionsRow; } + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/FocusManager.cpp b/wlx/wlxbase_wlqt/src/FocusManager.cpp new file mode 100644 index 0000000..3087136 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/FocusManager.cpp @@ -0,0 +1,332 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +bool FocusManager::s_reloadFocusTarget = false; + +FocusManager::FocusManager(QWidget *pluginRoot, QWidget *primaryView, QObject *parent) + : QObject(parent) + , m_pluginRoot(pluginRoot) + , m_primaryView(primaryView) + , m_isActive(false) + , m_undoStack(nullptr) + , m_nextShortcutId(1) +{ + installFocusGuard(); + + // Detect focus entering/leaving the plugin hierarchy + connect(qApp, &QApplication::focusChanged, this, [this](QWidget *old, QWidget *now) { + // Skip focus management when a modal dialog (save dialog, message box) + // is active — calling setActive(false) during a modal causes crashes. + if (QApplication::activeModalWidget()) + return; + + bool oldInside = old && m_pluginRoot && (old == m_pluginRoot || m_pluginRoot->isAncestorOf(old)); + bool nowInside = now && m_pluginRoot && (now == m_pluginRoot || m_pluginRoot->isAncestorOf(now)); + + if (m_isActive) { + if (oldInside && !nowInside) { + setActive(false); + } + } else { + if (nowInside && !oldInside) { + // Focus entering while inactive — bounce it back to the host + if (old) { + QPointer pOld(old); + QTimer::singleShot(0, this, [this, pOld]() { + if (pOld) { + QWidget *currentFocus = QApplication::focusWidget(); + if (currentFocus && (currentFocus == m_pluginRoot || + m_pluginRoot->isAncestorOf(currentFocus))) { + pOld->setFocus(Qt::OtherFocusReason); + } + } + }); + } else { + QTimer::singleShot(0, this, [this]() { + QWidget *currentFocus = QApplication::focusWidget(); + if (currentFocus && (currentFocus == m_pluginRoot || + m_pluginRoot->isAncestorOf(currentFocus))) { + restoreFocusToDC(); + } + }); + } + } + } + }); + + if (s_reloadFocusTarget) { + s_reloadFocusTarget = false; + QTimer::singleShot(50, this, [this]() { + setActive(true); + restoreViewFocus(); + }); + } +} + +FocusManager::~FocusManager() +{ + if (qApp) + qApp->removeEventFilter(this); +} + +// --- Activation --- + +bool FocusManager::isActive() const { return m_isActive; } + +void FocusManager::setActive(bool active) +{ + if (m_isActive == active) + return; + + m_isActive = active; + + if (!active) { + m_activeInput = nullptr; + m_pluginRoot->clearFocus(); + if (m_pluginRoot->parentWidget()) + m_pluginRoot->parentWidget()->setFocus(Qt::OtherFocusReason); + emit deactivated(); + } else { + emit activated(); + } +} + +// --- Input widget tracking --- + +void FocusManager::addInputWidget(QWidget *w) +{ + if (w) + m_extraInputWidgets.insert(w); +} + +void FocusManager::removeInputWidget(QWidget *w) +{ + m_extraInputWidgets.remove(w); +} + +bool FocusManager::isInputWidget(QWidget *w) const +{ + if (!w) + return false; + if (m_extraInputWidgets.contains(w)) + return true; + // A descendant of primaryView (but not primaryView itself) is an input widget + // (e.g. a cell editor spawned inside a QTableWidget) + return w != m_primaryView && m_primaryView->isAncestorOf(w); +} + +QWidget *FocusManager::activeInput() const { return m_activeInput; } + +// --- Focus proxy --- + +void FocusManager::setFocusProxy(QWidget *proxy) +{ + if (m_pluginRoot) + m_pluginRoot->setFocusProxy(proxy); +} + +void FocusManager::resetFocusProxy() +{ + if (m_pluginRoot && m_primaryView) + m_pluginRoot->setFocusProxy(m_primaryView); +} + +// --- Shortcut registration --- + +FocusManager::ShortcutId FocusManager::registerShortcut( + const QKeySequence &keys, ShortcutContext ctx, std::function handler) +{ + ShortcutId id = m_nextShortcutId++; + m_shortcuts.append({id, keys, ctx, std::move(handler)}); + return id; +} + +void FocusManager::unregisterShortcut(ShortcutId id) +{ + m_shortcuts.erase( + std::remove_if(m_shortcuts.begin(), m_shortcuts.end(), + [id](const RegisteredShortcut &s) { return s.id == id; }), + m_shortcuts.end()); +} + +// --- Optional undo/redo --- + +void FocusManager::setUndoStack(QUndoStack *stack) +{ + // Unregister previous undo shortcuts + for (ShortcutId id : m_undoShortcutIds) + unregisterShortcut(id); + m_undoShortcutIds.clear(); + + m_undoStack = stack; + + if (stack) { + // Ctrl+Z → undo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_Z), WhenNoInput, + [stack]() { + if (stack->canUndo()) { stack->undo(); return true; } + return false; + })); + + // Ctrl+Shift+Z → redo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_Z), WhenNoInput, + [stack]() { + if (stack->canRedo()) { stack->redo(); return true; } + return false; + })); + + // Ctrl+Y → redo + m_undoShortcutIds.append(registerShortcut( + QKeySequence(Qt::CTRL | Qt::Key_Y), WhenNoInput, + [stack]() { + if (stack->canRedo()) { stack->redo(); return true; } + return false; + })); + } +} + +QUndoStack *FocusManager::undoStack() const { return m_undoStack; } + +// --- Saved focus --- + +void FocusManager::saveFocusWidget(QWidget *w) { m_savedFocusWidget = w; } + +// --- Access --- + +QWidget *FocusManager::pluginRoot() const { return m_pluginRoot; } +QWidget *FocusManager::primaryView() const { return m_primaryView; } + +// --- Focus restoration --- + +void FocusManager::restoreViewFocus() +{ + if (m_primaryView) + m_primaryView->setFocus(Qt::OtherFocusReason); +} + +void FocusManager::expectReloadFocus() +{ + s_reloadFocusTarget = true; + QTimer::singleShot(1000, []() { + s_reloadFocusTarget = false; + }); +} + +void FocusManager::restoreFocusToDC() +{ + if (m_savedFocusWidget) { + m_savedFocusWidget->setFocus(Qt::OtherFocusReason); + } else if (m_pluginRoot) { + if (QWidget *fw = QApplication::focusWidget()) { + if (fw == m_pluginRoot || fw->isAncestorOf(m_pluginRoot) || + m_pluginRoot->isAncestorOf(fw)) + fw->clearFocus(); + } + } +} + +void FocusManager::installFocusGuard() +{ + if (qApp) + qApp->installEventFilter(this); + if (m_pluginRoot && m_primaryView) + m_pluginRoot->setFocusProxy(m_primaryView); +} + +// --- Event filter (the critical focus/shortcut engine) --- + +bool FocusManager::eventFilter(QObject *obj, QEvent *event) +{ + QWidget *w = qobject_cast(obj); + + // --- Geometry-based click detection --- + if (event->type() == QEvent::MouseButtonPress) { + if (!m_pluginRoot) return false; + + auto *me = static_cast(event); + const QPoint gp = me->globalPosition().toPoint(); + const QRect gr(m_pluginRoot->mapToGlobal(QPoint(0, 0)), m_pluginRoot->size()); + + if (m_isActive && !gr.contains(gp)) { + // Click outside plugin — deactivate + setActive(false); + return false; + } else if (!m_isActive && gr.contains(gp)) { + // Click inside plugin — activate + m_isActive = true; + emit activated(); + if (w && (w->focusPolicy() & Qt::ClickFocus)) { + w->setFocus(Qt::MouseFocusReason); + } else if (m_primaryView) { + m_primaryView->setFocus(Qt::MouseFocusReason); + } + } + } + + // --- FocusIn tracking --- + if (event->type() == QEvent::FocusIn) { + if (w && m_pluginRoot && (w == m_pluginRoot || m_pluginRoot->isAncestorOf(w))) { + auto *fe = static_cast(event); + if (!m_isActive && fe->reason() == Qt::OtherFocusReason) { + // Programmatic focus entry while inactive — don't activate + return false; + } + if (!m_isActive) { + m_isActive = true; + emit activated(); + } + if (isInputWidget(w)) { + m_activeInput = w; + emit inputWidgetEntered(w); + } + } + } + + // --- KeyPress: shortcut dispatch --- + if (event->type() == QEvent::KeyPress && m_isActive) { + auto *ke = static_cast(event); + QKeySequence pressed(ke->modifiers() | ke->key()); + + for (const auto &shortcut : m_shortcuts) { + if (shortcut.keys.count() != 1) + continue; + if (pressed[0] != shortcut.keys[0]) + continue; + + // Check context + if (shortcut.ctx == WhenNoInput && m_activeInput) + continue; + + if (shortcut.handler && shortcut.handler()) + return true; + } + } + + // --- ChildAdded: NoFocus enforcement --- + if (event->type() == QEvent::ChildAdded) { + if (w && m_pluginRoot && (w == m_pluginRoot || m_pluginRoot->isAncestorOf(w))) { + auto *ce = static_cast(event); + if (auto *childWidget = qobject_cast(ce->child())) { + if (!isInputWidget(childWidget)) + childWidget->setFocusPolicy(Qt::NoFocus); + } + } + } + + return QObject::eventFilter(obj, event); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginSplitView.cpp b/wlx/wlxbase_wlqt/src/PluginSplitView.cpp new file mode 100644 index 0000000..cf02f26 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginSplitView.cpp @@ -0,0 +1,34 @@ +#include + +namespace QtWlPlugin { + +PluginSplitView::PluginSplitView(QWidget *leftPanel, QWidget *rightContent, + QWidget *parent) + : QSplitter(Qt::Horizontal, parent) + , m_left(leftPanel) + , m_right(rightContent) +{ + addWidget(m_left); + addWidget(m_right); + + m_left->setMinimumWidth(100); + m_left->setMaximumWidth(350); + + setStretchFactor(0, 0); // Left: fixed + setStretchFactor(1, 1); // Right: stretches + + setSizes({180, 600}); + setChildrenCollapsible(false); + + setHandleWidth(3); +} + +void PluginSplitView::setLeftWidth(int pixels) +{ + setSizes({pixels, width() - pixels}); +} + +QWidget *PluginSplitView::leftPanel() const { return m_left; } +QWidget *PluginSplitView::rightContent() const { return m_right; } + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp b/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp new file mode 100644 index 0000000..d2be345 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginStatusBar.cpp @@ -0,0 +1,136 @@ +#include + +#include +#include +#include + +namespace QtWlPlugin { + +PluginStatusBar::PluginStatusBar(QWidget *parent) + : QWidget(parent) + , m_layout(new QHBoxLayout(this)) +{ + m_layout->setContentsMargins(4, 2, 4, 2); + m_layout->setSpacing(0); + setFixedHeight(22); + setStyleSheet(QStringLiteral( + "PluginStatusBar { border-top: 1px solid #c0c0c0; }" + ).replace(QStringLiteral("PluginStatusBar"), + QStringLiteral("QtWlPlugin--PluginStatusBar"))); + + QFont smallFont = font(); + smallFont.setPointSize(9); + + m_encodingLabel = new QLabel(this); + m_encodingLabel->setFont(smallFont); + + m_formatLabel = new QLabel(this); + m_formatLabel->setFont(smallFont); + + m_rowLabel = new QLabel(this); + m_rowLabel->setFont(smallFont); + + rebuild(); +} + +QFrame *PluginStatusBar::createSeparator() +{ + auto *sep = new QFrame(this); + sep->setFrameShape(QFrame::VLine); + sep->setFrameShadow(QFrame::Sunken); + sep->setFixedWidth(2); + return sep; +} + +void PluginStatusBar::rebuild() +{ + // Remove all items from layout (without deleting the labels themselves) + while (m_layout->count() > 0) { + QLayoutItem *item = m_layout->takeAt(0); + // Delete separators and spacers, keep our persistent labels + if (item->widget() + && item->widget() != m_encodingLabel + && item->widget() != m_formatLabel + && item->widget() != m_rowLabel + && !m_extras.values().contains(qobject_cast(item->widget()))) { + delete item->widget(); + } + delete item; + } + + // Add extras first (sorted by key) + QStringList keys = m_extras.keys(); + keys.sort(); + for (const auto &key : keys) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_extras[key]); + } + + // Encoding + if (!m_encodingLabel->text().isEmpty()) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_encodingLabel); + } + + // Format + if (!m_formatLabel->text().isEmpty()) { + if (m_layout->count() > 0) + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_formatLabel); + } + + // Spacer → row count on right + m_layout->addStretch(1); + + if (!m_rowLabel->text().isEmpty()) { + m_layout->addWidget(createSeparator()); + m_layout->addWidget(m_rowLabel); + } +} + +void PluginStatusBar::setEncoding(const QString &encoding) +{ + m_encodingLabel->setText(QStringLiteral(" %1 ").arg(encoding)); + rebuild(); +} + +void PluginStatusBar::setFormatInfo(const QString &info) +{ + m_formatLabel->setText(QStringLiteral(" %1 ").arg(info)); + rebuild(); +} + +void PluginStatusBar::setRowCount(int filtered, int total) +{ + if (filtered == total) + m_rowLabel->setText(QStringLiteral(" Rows: %1 ").arg(total)); + else + m_rowLabel->setText(QStringLiteral(" Rows: %1/%2 ").arg(filtered).arg(total)); + rebuild(); +} + +void PluginStatusBar::setExtraInfo(const QString &key, const QString &value) +{ + QLabel *label = m_extras.value(key, nullptr); + if (!label) { + QFont smallFont = font(); + smallFont.setPointSize(9); + label = new QLabel(this); + label->setFont(smallFont); + m_extras[key] = label; + } + label->setText(QStringLiteral(" %1: %2 ").arg(key, value)); + rebuild(); +} + +void PluginStatusBar::removeExtraInfo(const QString &key) +{ + if (m_extras.contains(key)) { + delete m_extras.take(key); + rebuild(); + } +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/PluginToolBar.cpp b/wlx/wlxbase_wlqt/src/PluginToolBar.cpp new file mode 100644 index 0000000..4d422d7 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/PluginToolBar.cpp @@ -0,0 +1,160 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace QtWlPlugin { + +PluginToolBar::PluginToolBar(FocusManager *fm, QWidget *parent) + : QToolBar(parent) + , m_fm(fm) +{ + setFocusPolicy(Qt::NoFocus); + setStyleSheet( + "QToolBar { spacing: 2px; }" + "QToolButton { padding: 2px 4px; margin: 1px; }" + ); + enforceNoFocus(); +} + +static QIcon iconFromText(const QString &text, QWidget *parent) +{ + QPixmap pixmap(32, 32); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + + QFont font = parent ? parent->font() : QFont(); + font.setPixelSize(22); + painter.setFont(font); + + QColor color = Qt::black; + if (parent) { + color = parent->palette().color(QPalette::ButtonText); + } + painter.setPen(color); + painter.drawText(pixmap.rect(), Qt::AlignCenter, text); + painter.end(); + + return QIcon(pixmap); +} + +static QString getFallbackUnicodeIcon(const QString &systemIconName) +{ + if (systemIconName == QStringLiteral("document-save")) + return QStringLiteral("🖫"); + if (systemIconName == QStringLiteral("document-save-as")) + return QStringLiteral("🖪"); + if (systemIconName == QStringLiteral("edit-undo")) + return QStringLiteral("↶"); + if (systemIconName == QStringLiteral("edit-redo")) + return QStringLiteral("↷"); + if (systemIconName == QStringLiteral("document-print")) + return QString::fromUtf8("\xf0\x9f\x96\xa8\xef\xb8\x8e"); // 🖨︎ + if (systemIconName == QStringLiteral("view-refresh")) + return QStringLiteral("⟳"); + if (systemIconName == QStringLiteral("visibility")) + return QString::fromUtf8("\xf0\x9f\x91\x81\xef\xb8\x8e"); // 👁︎ + if (systemIconName == QStringLiteral("format-text-direction-ltr")) + return QString::fromUtf8("\xe2\x86\xa9\xef\xb8\x8e"); // ↩︎ + if (systemIconName == QStringLiteral("document-open")) + return QString::fromUtf8("\xe2\x86\x97\xef\xb8\x8e"); // ↗︎ + if (systemIconName == QStringLiteral("edit-find")) + return QString::fromUtf8("\xf0\x9f\x94\x8d\xef\xb8\x8e"); // 🔍︎ + if (systemIconName == QStringLiteral("border-all")) + return QString::fromUtf8("\xe2\x96\xa6"); // ▦ + return QString(); +} + +QAction *PluginToolBar::addToolAction(const QString &text, + const QKeySequence &shortcut, + int ctx, + const QString &systemIconName, + const QString &unicodeIcon, + ButtonDisplay display, + IconMode iconMode) +{ + QIcon icon; + QString resolvedUnicode = unicodeIcon; + if (resolvedUnicode.isEmpty() && !systemIconName.isEmpty()) { + resolvedUnicode = getFallbackUnicodeIcon(systemIconName); + } + + if (iconMode == IconMode::System) { + if (!systemIconName.isEmpty()) { + icon = QIcon::fromTheme(systemIconName); + } + if (icon.isNull() && !resolvedUnicode.isEmpty()) { + icon = iconFromText(resolvedUnicode, this); + } + } else { // IconMode::Unicode + if (!resolvedUnicode.isEmpty()) { + icon = iconFromText(resolvedUnicode, this); + } + } + + QAction *action = new QAction(text, this); + if (!icon.isNull()) { + action->setIcon(icon); + } + action->setProperty("buttonDisplay", static_cast(display)); + addAction(action); + + if (!shortcut.isEmpty()) { + action->setToolTip(text + " (" + shortcut.toString(QKeySequence::NativeText) + ")"); + + if (m_fm) { + m_fm->registerShortcut( + shortcut, static_cast(ctx), + [action]() { action->trigger(); return true; }); + } + } else { + action->setToolTip(text); + } + + // Restore focus to primary view after action trigger + connect(action, &QAction::triggered, this, [this]() { + QTimer::singleShot(0, this, [this]() { + if (m_fm) + m_fm->restoreViewFocus(); + }); + }); + + return action; +} + +void PluginToolBar::actionEvent(QActionEvent *event) +{ + QToolBar::actionEvent(event); + enforceNoFocus(); +} + +void PluginToolBar::enforceNoFocus() +{ + for (QAction *action : actions()) { + QWidget *w = widgetForAction(action); + if (w) { + w->setFocusPolicy(Qt::NoFocus); + if (auto *btn = qobject_cast(w)) { + QVariant val = action->property("buttonDisplay"); + if (val.isValid()) { + auto display = static_cast(val.toInt()); + if (display == ButtonDisplay::IconOnly) { + btn->setToolButtonStyle(Qt::ToolButtonIconOnly); + } else if (display == ButtonDisplay::TextOnly) { + btn->setToolButtonStyle(Qt::ToolButtonTextOnly); + } else { + btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + } + } + } + } + } +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp b/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp new file mode 100644 index 0000000..d611b08 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/ScopedFindReplacePanel.cpp @@ -0,0 +1,34 @@ +#include +#include + +#include +#include +#include + +namespace QtWlPlugin { + +ScopedFindReplacePanel::ScopedFindReplacePanel(FocusManager *fm, QWidget *parent) + : FindReplacePanel(fm, parent) +{ + auto *lblScope = new QLabel("Scope:", this); + m_comboScope = new QComboBox(this); + m_comboScope->setFocusPolicy(Qt::NoFocus); + + // Insert at the beginning of the options row (before checkboxes) + QHBoxLayout *row = optionsRow(); + row->insertWidget(0, lblScope); + row->insertWidget(1, m_comboScope); +} + +void ScopedFindReplacePanel::setScopes(const QStringList &scopes) +{ + m_comboScope->clear(); + m_comboScope->addItems(scopes); +} + +QString ScopedFindReplacePanel::currentScope() const +{ + return m_comboScope->currentText(); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp b/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp new file mode 100644 index 0000000..e6aac0f --- /dev/null +++ b/wlx/wlxbase_wlqt/src/SequentialRowProxyModel.cpp @@ -0,0 +1,22 @@ +#include + +namespace QtWlPlugin { + +SequentialRowProxyModel::SequentialRowProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +SequentialRowProxyModel::~SequentialRowProxyModel() +{ +} + +QVariant SequentialRowProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical && role == Qt::DisplayRole) { + return section + 1; + } + return QSortFilterProxyModel::headerData(section, orientation, role); +} + +} // namespace QtWlPlugin diff --git a/wlx/wlxbase_wlqt/src/ThemeManager.cpp b/wlx/wlxbase_wlqt/src/ThemeManager.cpp new file mode 100644 index 0000000..e5783a8 --- /dev/null +++ b/wlx/wlxbase_wlqt/src/ThemeManager.cpp @@ -0,0 +1,282 @@ +#include + +#include +#include +#include +#include + +namespace QtWlPlugin { + +ThemeManager::Theme ThemeManager::s_current = ThemeManager::Light; + +void ThemeManager::applyTheme(QWidget *root, Theme theme) +{ + s_current = theme; + + if (theme == Light) { + root->setStyleSheet(QString()); + } else { + root->setStyleSheet(darkStylesheet()); + } + + // Persist + QSettings settings(QStringLiteral("QtWlPlugin"), QStringLiteral("Preferences")); + settings.setValue(QStringLiteral("theme"), theme == Dark ? QStringLiteral("dark") : QStringLiteral("light")); +} + +ThemeManager::Theme ThemeManager::currentTheme() +{ + bool dark = true; // Default to dark + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + if (const auto *hints = QGuiApplication::styleHints()) { + auto scheme = hints->colorScheme(); + if (scheme == Qt::ColorScheme::Light) { + dark = false; + } else if (scheme == Qt::ColorScheme::Dark) { + dark = true; + } else { + dark = true; + } + } +#else + QPalette pal = QGuiApplication::palette(); + if (pal.color(QPalette::Window).value() < 120) { + dark = true; + } else { + dark = true; + } +#endif + + s_current = dark ? Dark : Light; + return s_current; +} + +void ThemeManager::toggleTheme(QWidget *root) +{ + applyTheme(root, s_current == Light ? Dark : Light); +} + +bool ThemeManager::isDark() +{ + return s_current == Dark; +} + +QString ThemeManager::darkStylesheet() +{ + return QStringLiteral(R"( + QWidget { + background-color: #1e1e1e; + color: #d4d4d4; + selection-background-color: #264f78; + selection-color: #ffffff; + } + + QTableView { + background-color: #1e1e1e; + alternate-background-color: #252526; + gridline-color: #3c3c3c; + border: 1px solid #3c3c3c; + } + + QHeaderView::section { + background-color: #333333; + color: #d4d4d4; + border: 1px solid #3c3c3c; + padding: 3px 5px; + font-weight: bold; + } + + QHeaderView::section:hover { + background-color: #404040; + } + + QTreeView { + background-color: #252526; + alternate-background-color: #2d2d2d; + border: 1px solid #3c3c3c; + } + + QTreeView::item:selected { + background-color: #264f78; + } + + QTreeView::item:hover { + background-color: #2a2d2e; + } + + QListWidget { + background-color: #252526; + border: 1px solid #3c3c3c; + } + + QListWidget::item:selected { + background-color: #264f78; + } + + QListWidget::item:hover { + background-color: #2a2d2e; + } + + QToolBar { + background-color: #333333; + border-bottom: 1px solid #3c3c3c; + spacing: 2px; + } + + QToolBar QLabel { + color: #d4d4d4; + } + + QToolButton { + background-color: transparent; + color: #d4d4d4; + border: 1px solid transparent; + padding: 3px 6px; + border-radius: 3px; + } + + QToolButton:hover { + background-color: #404040; + border-color: #505050; + } + + QToolButton:checked { + background-color: #264f78; + border-color: #3a7bd5; + } + + QLineEdit { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #555555; + padding: 2px 4px; + border-radius: 2px; + } + + QLineEdit:focus { + border-color: #007acc; + } + + QComboBox { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #555555; + padding: 2px 4px; + border-radius: 2px; + } + + QComboBox::drop-down { + border-left: 1px solid #555555; + } + + QComboBox QAbstractItemView { + background-color: #252526; + color: #d4d4d4; + selection-background-color: #264f78; + } + + QMenu { + background-color: #252526; + color: #d4d4d4; + border: 1px solid #3c3c3c; + } + + QMenu::item:selected { + background-color: #264f78; + } + + QMenu::separator { + height: 1px; + background: #3c3c3c; + margin: 4px 8px; + } + + QTabWidget::pane { + border: 1px solid #3c3c3c; + background-color: #1e1e1e; + } + + QTabBar::tab { + background-color: #2d2d2d; + color: #969696; + border: 1px solid #3c3c3c; + padding: 4px 12px; + margin-right: 1px; + } + + QTabBar::tab:selected { + background-color: #1e1e1e; + color: #d4d4d4; + border-bottom-color: #1e1e1e; + } + + QTabBar::tab:hover { + background-color: #353535; + } + + QPlainTextEdit { + background-color: #1e1e1e; + color: #d4d4d4; + border: 1px solid #3c3c3c; + font-family: "Cascadia Code", "Fira Code", "Source Code Pro", monospace; + } + + QSplitter::handle { + background-color: #3c3c3c; + } + + QSplitter::handle:hover { + background-color: #007acc; + } + + QScrollBar:vertical { + background-color: #1e1e1e; + width: 12px; + border: none; + } + + QScrollBar::handle:vertical { + background-color: #424242; + min-height: 20px; + border-radius: 3px; + margin: 2px; + } + + QScrollBar::handle:vertical:hover { + background-color: #555555; + } + + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; + } + + QScrollBar:horizontal { + background-color: #1e1e1e; + height: 12px; + border: none; + } + + QScrollBar::handle:horizontal { + background-color: #424242; + min-width: 20px; + border-radius: 3px; + margin: 2px; + } + + QScrollBar::handle:horizontal:hover { + background-color: #555555; + } + + QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { + width: 0px; + } + + /* Status bar */ + QFrame[frameShape="5"] { + color: #555555; + } + )"); +} + +} // namespace QtWlPlugin