From 74bbc4dfd16f54af8b856b85c9b26610d4e06c53 Mon Sep 17 00:00:00 2001 From: Falk Elvedal Bruskeland <66320400+falbru@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:16:03 +0200 Subject: [PATCH] Add vertical and horizontal splits (#38) --- CMakeLists.txt | 2 + rc/kakoune-qt.kak | 13 +++-- src/container.cpp | 71 ++++++++++++++++++++++++++ src/container.hpp | 32 ++++++++++++ src/ipc.cpp | 9 ++-- src/ipc.hpp | 2 +- src/main.cpp | 10 +++- src/mainwindow.cpp | 113 ++++++++++++++++++++++++++--------------- src/mainwindow.hpp | 11 ++-- src/splitcontainer.cpp | 23 +++++++++ src/splitcontainer.hpp | 19 +++++++ 11 files changed, 251 insertions(+), 54 deletions(-) create mode 100644 src/container.cpp create mode 100644 src/container.hpp create mode 100644 src/splitcontainer.cpp create mode 100644 src/splitcontainer.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c04b1ec..4ea895a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,8 @@ qt_add_executable(kak-qt src/keybindings.cpp src/drawoptions.cpp src/ipc.cpp + src/container.cpp + src/splitcontainer.cpp src/kakounesession.cpp src/kakouneclient.cpp src/kakounemenu.cpp diff --git a/rc/kakoune-qt.kak b/rc/kakoune-qt.kak index db6101b..f8635f2 100644 --- a/rc/kakoune-qt.kak +++ b/rc/kakoune-qt.kak @@ -4,10 +4,17 @@ hook global SessionRenamed .*:.* %{ nop %sh{ KAKQT_SESSION_ID=$kak_client_env_KAKQT_SESSION_ID kak-qt cli rename-session $kak_session } } -define-command -override -docstring "new []: create a new Kakoune client" new -params .. %{ nop %sh{ - KAKQT_SESSION_ID=$kak_client_env_KAKQT_SESSION_ID kak-qt cli new-client $@ +define-command kakqt-split-horizontal -params .. -docstring "kakqt-split-horizontal []: create a new Kakoune client" %{ nop %sh{ + KAKQT_WINDOW_ID=$kak_client_env_KAKQT_WINDOW_ID KAKQT_SESSION_ID=$kak_client_env_KAKQT_SESSION_ID kak-qt cli split-horizontal $@ }} -complete-command -menu new command +complete-command -menu kakqt-split-horizontal command + +define-command kakqt-split-vertical -params .. -docstring "kakqt-split-vertical []: create a new Kakoune client" %{ nop %sh{ + KAKQT_WINDOW_ID=$kak_client_env_KAKQT_WINDOW_ID KAKQT_SESSION_ID=$kak_client_env_KAKQT_SESSION_ID kak-qt cli split-vertical $@ +}} +complete-command -menu kakqt-split-vertical command + +alias global new kakqt-split-horizontal define-command kakqt-focus -params ..1 -docstring ' kakqt-focus []: focus the given client diff --git a/src/container.cpp b/src/container.cpp new file mode 100644 index 0000000..d78af80 --- /dev/null +++ b/src/container.cpp @@ -0,0 +1,71 @@ +#include "container.hpp" +#include "kakounewidget.hpp" +#include +#include + +Container::Container(Qt::Orientation orientation, QWidget *parent) : QWidget(parent) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_splitter = new QSplitter(this); + m_splitter->setOrientation(orientation); + + layout->addWidget(m_splitter); +} + +void Container::addWidget(QWidget *widget) +{ + connectWidget(widget); + m_splitter->addWidget(widget); +} + +Qt::Orientation Container::getOrientation() const +{ + return m_splitter->orientation(); +} + +void Container::connectWidget(QWidget *widget) +{ + if (KakouneWidget *kak_widget = qobject_cast(widget)) + connectKakouneWidget(kak_widget); + + connect(widget, &QWidget::destroyed, this, [=]() { + if (m_splitter->count() == 0) + { + deleteLater(); + } + else + { + m_splitter->widget(0)->setFocus(); + } + }); +} + +void Container::connectKakouneWidget(KakouneWidget *kak_widget) +{ + connect(kak_widget, &KakouneWidget::finished, this, [=]() { + kak_widget->setParent(nullptr); + kak_widget->deleteLater(); + }); +} + +void Container::focusInEvent(QFocusEvent *event) +{ + m_splitter->widget(0)->setFocus(); +} + +Container *Container::findParentContainer(QWidget *widget) +{ + QWidget *parent_container = widget->parentWidget(); + while (parent_container) + { + Container *container = qobject_cast(parent_container); + if (container) + { + return container; + } + parent_container = parent_container->parentWidget(); + } + return nullptr; +} diff --git a/src/container.hpp b/src/container.hpp new file mode 100644 index 0000000..2b7aca2 --- /dev/null +++ b/src/container.hpp @@ -0,0 +1,32 @@ +#ifndef CONTAINER_HPP +#define CONTAINER_HPP + +#include "kakounewidget.hpp" +#include +#include +#include +#include + +class Container : public QWidget +{ + Q_OBJECT + + public: + Container(Qt::Orientation orientation = Qt::Horizontal, QWidget *parent = nullptr); + + void addWidget(QWidget *widget); + + Qt::Orientation getOrientation() const; + + static Container *findParentContainer(QWidget *widget); + + protected: + void connectWidget(QWidget *widget); + void connectKakouneWidget(KakouneWidget *kak_widget); + + void focusInEvent(QFocusEvent *event) override; + + QSplitter *m_splitter; +}; + +#endif diff --git a/src/ipc.cpp b/src/ipc.cpp index c7088ca..52ee7c2 100644 --- a/src/ipc.cpp +++ b/src/ipc.cpp @@ -1,5 +1,6 @@ #include "ipc.hpp" #include +#include namespace KakouneIPC { @@ -57,7 +58,7 @@ IPCServer::~IPCServer() void IPCServer::bind(MainWindow *main_window) { - connect(this, &IPCServer::newClient, main_window, &MainWindow::newClient); + connect(this, &IPCServer::newSplit, main_window, &MainWindow::newSplit); connect(this, &IPCServer::focusWindow, main_window, &MainWindow::focusWindow); connect(this, &IPCServer::renameSession, main_window, &MainWindow::renameSession); } @@ -82,10 +83,10 @@ void IPCServer::handleConnection() void IPCServer::handleCommand(QJsonObject request) { const QString method = request["method"].toString(); - qDebug() << method; - if (method == "newClient") + if (method == "newSplit") { - emit newClient(request["args"].isString() ? request["args"].toString() : ""); + emit newSplit(request["client_name"].toString(), request["args"].isString() ? request["args"].toString() : "", + request["orientation"].toString() == "vertical" ? Qt::Vertical : Qt::Horizontal); } else if (method == "focusWindow") { diff --git a/src/ipc.hpp b/src/ipc.hpp index 370937c..b7df184 100644 --- a/src/ipc.hpp +++ b/src/ipc.hpp @@ -30,7 +30,7 @@ class IPCServer : public QObject void bind(MainWindow *main_window); signals: - void newClient(const QString &arguments); + void newSplit(const QString &client_name, const QString &arguments, const Qt::Orientation &orientation); void focusWindow(const QString &client_name); void renameSession(const QString &session_name); diff --git a/src/main.cpp b/src/main.cpp index 66a79c5..40dc583 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,10 @@ #include #include #include +#include #include +#include +#include int main(int argc, char *argv[]) { @@ -36,9 +39,12 @@ int main(int argc, char *argv[]) const QString subcommand = positional_arguments[0]; - if (subcommand == "new-client") + if (subcommand == "split-horizontal" || subcommand == "split-vertical") { - ipc.send("newClient", {{"args", positional_arguments.mid(1).join(" ")}}); + auto orientation = subcommand == "split-vertical" ? "vertical" : "horizontal"; + ipc.send("newSplit", {{"args", positional_arguments.mid(1).join(" ")}, + {"client_name", QProcessEnvironment::systemEnvironment().value("KAKQT_WINDOW_ID")}, + {"orientation", orientation}}); } else if (subcommand == "focus") { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index fe854cc..512f8f7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6 +1,11 @@ #include "mainwindow.hpp" +#include "container.hpp" +#include "kakounewidget.hpp" #include "keybindings.hpp" +#include +#include #include +#include MainWindow::MainWindow(QString session_id, QWidget *parent) : QMainWindow(parent) { @@ -12,11 +17,13 @@ MainWindow::MainWindow(QString session_id, QWidget *parent) : QMainWindow(parent m_session = new KakouneSession(session_id); - m_root = new QSplitter(this); - this->newClient(""); + m_root = new SplitContainer(Qt::Horizontal, this); + setCentralWidget(m_root); + + KakouneWidget *kak_widget = createKakouneWidget(); + m_root->addWidget(kak_widget); updateWindowTitle(); - setCentralWidget(m_root); } MainWindow::~MainWindow() @@ -33,58 +40,51 @@ QUuid MainWindow::getID() return m_id; } -void MainWindow::newClient(const QString &arguments) +void MainWindow::newSplit(const QString &client_name, const QString &arguments, const Qt::Orientation &orientation) { - KakouneWidget *kakwidget = new KakouneWidget(m_session->getSessionId(), m_id, m_draw_options, arguments, m_root); - kakwidget->installEventFilter(new KeyBindingsFilter(this)); - connect(kakwidget, &KakouneWidget::finished, m_root, [=]() { - kakwidget->setParent(nullptr); - m_windows.removeOne(kakwidget); - - if (m_root->count() == 0) - { - close(); - } - }); - - m_windows.append(kakwidget); - m_root->addWidget(kakwidget); -} + QWidget *source_widget = findKakouneWidget(client_name); + if (source_widget == nullptr) + { + qWarning() << "MainWindow::newSplit: Could not find KakouneWidget with client name: " << client_name; + return; + } -void MainWindow::focusWindow(const QString &uuid) -{ - for (KakouneWidget *window : m_windows) + if (SplitContainer *parent_split = (SplitContainer *)Container::findParentContainer(source_widget)) { - if (window->getID().toString() == uuid) - { - window->setFocus(); - return; - } + KakouneWidget *new_kak_widget = createKakouneWidget(arguments); + parent_split->split(source_widget, new_kak_widget, orientation); + new_kak_widget->setFocus(); + } + else + { + qWarning("MainWindow::newSplit: Couldn't find parent container"); } } -void MainWindow::focusLeft() +void MainWindow::focusWindow(const QString &client_name) { - QWidget *focused_widget = qApp->focusWidget(); - int index = m_root->indexOf((QWidget *)focused_widget->parent()); // TODO - if (index <= 0) + KakouneWidget *kak_widget = findKakouneWidget(client_name); + + if (kak_widget == nullptr) { + qWarning() << "MainWindow::focusWindow: Could not find KakouneWidget with client name: " << client_name; return; } - m_windows[index - 1]->setFocus(); + kak_widget->setFocus(); } -void MainWindow::focusRight() +void MainWindow::focusLeft() { - QWidget *focused_widget = qApp->focusWidget(); - int index = m_root->indexOf((QWidget *)focused_widget->parent()); // TODO - if (index >= m_windows.size() - 1) - { - return; - } + // TODO +} - m_windows[index + 1]->setFocus(); +void MainWindow::focusRight() +{ + // TODO + // Container: have a lastFocusedWidget() method + // Iterate up to closest container that is Horizontal. Focus lastFocusedWidget + // When a Container gains focus, focus the last focused widget } void MainWindow::updateWindowTitle() @@ -97,3 +97,36 @@ void MainWindow::renameSession(const QString &session_name) m_session->setSessionId(session_name); updateWindowTitle(); } + +KakouneWidget *MainWindow::findKakouneWidget(const QString &client_name) +{ + for (KakouneWidget *window : m_windows) + { + if (window->getID().toString() == client_name) + { + return window; + } + } + return nullptr; +} + +KakouneWidget *MainWindow::createKakouneWidget(const QString &arguments) +{ + KakouneWidget *kakwidget = new KakouneWidget(m_session->getSessionId(), m_id, m_draw_options, arguments, m_root); + + kakwidget->installEventFilter(new KeyBindingsFilter(this)); + + connect(kakwidget, &KakouneWidget::finished, m_root, [=]() { + m_windows.removeOne(kakwidget); + + if (m_windows.size() == 0) + { + close(); + return; + } + }); + + m_windows.append(kakwidget); + + return kakwidget; +} diff --git a/src/mainwindow.hpp b/src/mainwindow.hpp index 4189995..56520d7 100644 --- a/src/mainwindow.hpp +++ b/src/mainwindow.hpp @@ -4,6 +4,7 @@ #include "drawoptions.hpp" #include "kakounesession.hpp" #include "kakounewidget.hpp" +#include "splitcontainer.hpp" #include #include #include @@ -22,8 +23,8 @@ class MainWindow : public QMainWindow QUuid getID(); public slots: - void newClient(const QString &arguments); - void focusWindow(const QString &uuid); + void newSplit(const QString &client_name, const QString &arguments, const Qt::Orientation &orientation); + void focusWindow(const QString &client_name); void renameSession(const QString &session_name); protected: @@ -32,11 +33,13 @@ class MainWindow : public QMainWindow private: QUuid m_id; - QSplitter *m_root; KakouneSession *m_session; - QList m_windows; + KakouneWidget *createKakouneWidget(const QString &arguments = ""); + KakouneWidget *findKakouneWidget(const QString &client_name); + SplitContainer *m_root; + QList m_windows; DrawOptions *m_draw_options; }; diff --git a/src/splitcontainer.cpp b/src/splitcontainer.cpp new file mode 100644 index 0000000..6265470 --- /dev/null +++ b/src/splitcontainer.cpp @@ -0,0 +1,23 @@ +#include "splitcontainer.hpp" + +SplitContainer::SplitContainer(Qt::Orientation orientation, QWidget *parent) : Container(orientation, parent) +{ +} + +void SplitContainer::split(QWidget *source_widget, QWidget *new_widget, Qt::Orientation orientation) +{ + int index = m_splitter->indexOf(source_widget); + + SplitContainer *container = new SplitContainer(orientation, this); + m_splitter->replaceWidget( + index, + container); // NOTE: The container widget has to be added to the splitter before the source_widget is added to + // it. Or else the source widget will be removed from the splitter so that there is only one widget + // left, and then the splitter ratio won't be preserved when the container is added to the splitter. + + container->addWidget(source_widget); + container->addWidget(new_widget); + container->m_splitter->setSizes(QList({INT_MAX, INT_MAX})); + + connectWidget(container); +} diff --git a/src/splitcontainer.hpp b/src/splitcontainer.hpp new file mode 100644 index 0000000..551fcd7 --- /dev/null +++ b/src/splitcontainer.hpp @@ -0,0 +1,19 @@ +#ifndef SPLITCONTAINER_HPP +#define SPLITCONTAINER_HPP + +#include "container.hpp" +#include +#include +#include + +class SplitContainer : public Container +{ + Q_OBJECT + + public: + SplitContainer(Qt::Orientation orientation, QWidget *parent = nullptr); + + void split(QWidget *source_widget, QWidget *new_widget, Qt::Orientation orientation = Qt::Horizontal); +}; + +#endif