diff --git a/gui/ChatPane.qml b/gui/ChatPane.qml index 199eed2..96b9348 100644 --- a/gui/ChatPane.qml +++ b/gui/ChatPane.qml @@ -165,6 +165,42 @@ Item { Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid } + // Filter bar (Ctrl+F) + RowLayout { + id: filterBar + Layout.fillWidth: true + Layout.margins: 4 + spacing: 4 + visible: false + + TextField { + id: filterInput + Layout.fillWidth: true + placeholderText: "Filter messages..." + onTextChanged: paneModel.filterText = text + Keys.onEscapePressed: { + filterBar.visible = false + paneModel.filterText = "" + text = "" + } + } + + Label { + text: paneModel.filtering ? paneModel.rowCount() + " matches" : "" + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + ToolButton { + icon.name: "window-close" + onClicked: { + filterBar.visible = false + paneModel.filterText = "" + filterInput.text = "" + } + } + } + // Chat view Item { Layout.fillWidth: true @@ -529,6 +565,21 @@ Item { + // Filter shortcut + Shortcut { + sequence: "Ctrl+F" + onActivated: { + filterBar.visible = !filterBar.visible + if (filterBar.visible) { + filterInput.forceActiveFocus() + filterInput.selectAll() + } else { + paneModel.filterText = "" + filterInput.text = "" + } + } + } + // Sloppy focus — focus prompt when mouse hovers inside pane HoverHandler { id: sloppyHover diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 627c0c4..9cfb5b0 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -36,15 +36,17 @@ ChatBlockModel::ChatBlockModel(QObject *parent) int ChatBlockModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; - return m_blocks.size(); + const auto &blocks = m_filterText.isEmpty() ? m_blocks : m_filteredBlocks; + return blocks.size(); } QVariant ChatBlockModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= m_blocks.size()) + const auto &blocks = m_filterText.isEmpty() ? m_blocks : m_filteredBlocks; + if (!index.isValid() || index.row() >= blocks.size()) return {}; - const ChatBlock &block = m_blocks[index.row()]; + const ChatBlock &block = blocks[index.row()]; switch (role) { case BlockTypeRole: { switch (block.type) { @@ -108,9 +110,36 @@ void ChatBlockModel::setRawText(const QString &text) if (text == m_rawText) return; m_rawText = text; reparse(); + if (!m_filterText.isEmpty()) + applyFilter(); emit rawTextChanged(); } +void ChatBlockModel::setFilterText(const QString &text) +{ + if (text == m_filterText) return; + m_filterText = text; + applyFilter(); + emit filterTextChanged(); +} + +void ChatBlockModel::applyFilter() +{ + beginResetModel(); + if (m_filterText.isEmpty()) { + m_filteredBlocks.clear(); + } else { + m_filteredBlocks.clear(); + for (const auto &block : m_allBlocks) { + if (block.content.contains(m_filterText, Qt::CaseInsensitive) || + block.name.contains(m_filterText, Qt::CaseInsensitive)) { + m_filteredBlocks.append(block); + } + } + } + endResetModel(); +} + static constexpr int INITIAL_BLOCKS = 20; static constexpr int LOAD_MORE_COUNT = 30; diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index eb12690..23fd929 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -41,6 +41,8 @@ class ChatBlockModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString rawText READ rawText WRITE setRawText NOTIFY rawTextChanged) + Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged) + Q_PROPERTY(bool filtering READ filtering NOTIFY filterTextChanged) public: enum Roles { @@ -60,6 +62,10 @@ public: QString rawText() const { return m_rawText; } void setRawText(const QString &text); + QString filterText() const { return m_filterText; } + void setFilterText(const QString &text); + bool filtering() const { return !m_filterText.isEmpty(); } + Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const; Q_INVOKABLE QString prettyJson(const QString &text) const; Q_INVOKABLE void copyToClipboard(const QString &text) const; @@ -71,16 +77,20 @@ public: signals: void rawTextChanged(); + void filterTextChanged(); private: void reparse(); void updateVisibleWindow(); + void applyFilter(); QList parseBlocks(const QString &text) const; QVariantList parseSubBlocks(const QString &content) const; QString m_rawText; + QString m_filterText; QList m_allBlocks; // all parsed blocks QList m_blocks; // visible window (tail of m_allBlocks) + QList m_filteredBlocks; // filtered subset when filterText is active int m_visibleStart = 0; // index into m_allBlocks where visible window starts mutable QHash m_highlightCache; }; diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 69e637c..41aef4a 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -770,6 +770,18 @@ ApplicationWindow { } } + Shortcut { + sequence: "Ctrl+F" + onActivated: { + var pane = mainContent.panes[mainContent.focusedPaneIndex] + if (!pane) return + var fb = pane.children[0] // ColumnLayout + // Toggle via the filterBar id inside the pane + // Since filterBar is inside the Component, access via pane property + pane.toggleFilter() + } + } + // Handle pane swap: find target pane at drop coordinates function handlePaneSwap(sourcePane, rootX, rootY) { for (var i = 0; i < mainContent.panes.length; i++) { @@ -845,6 +857,17 @@ ApplicationWindow { if (sessionId !== "") refreshPaneChat() } + function toggleFilter() { + filterBar.visible = !filterBar.visible + if (filterBar.visible) { + filterInput.forceActiveFocus() + filterInput.selectAll() + } else { + paneModel.filterText = "" + filterInput.text = "" + } + } + function refreshPaneChat() { paneModel.rawText = "" chatOffset = 0 @@ -973,6 +996,42 @@ ApplicationWindow { Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid } + // Filter bar (Ctrl+F) + RowLayout { + id: filterBar + Layout.fillWidth: true + Layout.margins: 4 + spacing: 4 + visible: false + + TextField { + id: filterInput + Layout.fillWidth: true + placeholderText: "Filter messages..." + onTextChanged: paneRoot.paneModel.filterText = text + Keys.onEscapePressed: { + filterBar.visible = false + paneRoot.paneModel.filterText = "" + text = "" + } + } + + Label { + text: paneRoot.paneModel.filtering ? paneRoot.paneModel.rowCount() + " matches" : "" + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + ToolButton { + icon.name: "window-close" + onClicked: { + filterBar.visible = false + paneRoot.paneModel.filterText = "" + filterInput.text = "" + } + } + } + // Chat view Item { Layout.fillWidth: true