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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/wlxplugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions wlx/structview/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
71 changes: 71 additions & 0 deletions wlx/structview/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
95 changes: 95 additions & 0 deletions wlx/structview/README.md
Original file line number Diff line number Diff line change
@@ -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)
110 changes: 110 additions & 0 deletions wlx/structview/include/StructViewWidget.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#pragma once

#include <QWidget>
#include <QTreeView>
#include <QTableView>
#include <QTabWidget>
#include <QPlainTextEdit>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <memory>

#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 &current, 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<TextFormatEngine> 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;
};
103 changes: 103 additions & 0 deletions wlx/structview/include/TextFormatEngine.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#pragma once

#include <QString>
#include <QByteArray>
#include <QVariant>
#include <QVector>
#include <QStringList>
#include <QList>
#include <memory>

/// 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<DocumentNode*> children;

// Grid data for this node
QStringList columnNames; ///< Column headers
QVector<QVector<QVariant>> 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<DocumentNode*>(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<TextFormatEngine> createForFile(const QString &filepath);
};
Loading