diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f71c2972a..b2c527d82 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(neovim-qt-gui popupmenumodel.cpp scrollbar.cpp shell.cpp + tabline.cpp treeview.cpp ${SRCS_PLATFORM} ${NEOVIM_RCC_SOURCES}) diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 5977e0a74..14f80f04b 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -13,8 +13,9 @@ static QString DefaultWindowTitle() noexcept return "Neovim"; } -MainWindow::MainWindow(NeovimConnector* c, QWidget* parent) - : QMainWindow(parent) +MainWindow::MainWindow(NeovimConnector* c, QWidget* parent) noexcept + : QMainWindow{ parent } + , m_tabline{ *c, this } , m_defaultFont{ font() } , m_defaultPalette{ palette() } { @@ -42,24 +43,7 @@ void MainWindow::init(NeovimConnector *c) m_shell = new Shell(c); - m_tabline_bar = addToolBar("tabline"); - m_tabline_bar->setObjectName("tabline"); - m_tabline_bar->setAllowedAreas(Qt::TopToolBarArea); - m_tabline_bar->setMovable(false); - m_tabline_bar->setFloatable(false); - // Avoid margins around the tabbar - m_tabline_bar->layout()->setContentsMargins(0, 0, 0, 0); - - m_tabline = new QTabBar(m_tabline_bar); - m_tabline->setDrawBase(false); - m_tabline->setExpanding(false); - m_tabline->setDocumentMode(true); - m_tabline->setFocusPolicy(Qt::NoFocus); - connect(m_tabline, &QTabBar::currentChanged, - this, &MainWindow::changeTab); - - m_tabline_bar->addWidget(m_tabline); - m_tabline_bar->setVisible(m_shell->GetShellOptions().IsTablineEnabled()); + addToolBar(&m_tabline); // Context menu and actions for right-click m_contextMenu = new QMenu(); @@ -123,12 +107,6 @@ void MainWindow::init(NeovimConnector *c) this, &MainWindow::neovimError); connect(m_shell, &Shell::neovimIsUnsupported, this, &MainWindow::neovimIsUnsupported); - connect(m_shell, &Shell::neovimExtTablineSet, - this, &MainWindow::extTablineSet); - connect(m_shell, &Shell::neovimTablineUpdate, - this, &MainWindow::neovimTablineUpdate); - connect(m_shell, &Shell::neovimShowtablineSet, - this, &MainWindow::neovimShowtablineSet); connect(m_shell, &Shell::neovimShowContextMenu, this, &MainWindow::neovimShowContextMenu); connect(m_actCut, &QAction::triggered, @@ -331,95 +309,18 @@ void MainWindow::handleNeovimAttachment(bool attached) { emit neovimAttachmentChanged(attached); - if (attached) { - if (isWindow() && m_shell != NULL) { - m_shell->updateGuiWindowState(windowState()); - } - } else { - m_tabline->deleteLater(); - m_tabline_bar->deleteLater(); - } -} - -Shell* MainWindow::shell() -{ - return m_shell; -} - -void MainWindow::extTablineSet(bool val) -{ - ShellOptions& shellOptions{ m_shell->GetShellOptions() }; - - // We can ignore events where the value does not change. - if (val == shellOptions.IsTablineEnabled()) { + if (!attached) { return; } - shellOptions.SetIsTablineEnabled(val); - m_nvim->api0()->vim_command("silent! redraw!"); - m_tabline_bar->setVisible(val); -} - -void MainWindow::neovimShowtablineSet(int val) -{ - m_shell->GetShellOptions().SetOptionShowTabline(val); + if (m_shell && isWindow()) { + m_shell->updateGuiWindowState(windowState()); + } } -void MainWindow::neovimTablineUpdate(int64_t curtab, QList tabs) +Shell* MainWindow::shell() { - if (!m_shell->GetShellOptions().IsTablineEnabled()) { - return; - } - - // remove extra tabs - for (int index=tabs.size(); indexcount(); index++) { - m_tabline->removeTab(index); - } - - for (int index=0; indexcount() <= index) { - m_tabline->addTab(text); - } else { - m_tabline->setTabText(index, text); - } - - m_tabline->setTabToolTip(index, text); - m_tabline->setTabData(index, QVariant::fromValue(tabs[index].tab)); - - if (curtab == tabs[index].tab) { - m_tabline->setCurrentIndex(index); - } - } - - Q_ASSERT(tabs.size() == m_tabline->count()); - - switch(m_shell->GetShellOptions().GetOptionShowTabline()) - { - // Never show tabline - case 0: - m_tabline_bar->setVisible(false); - break; - - // Show tabline for two or more tabs. - case 1: - m_tabline_bar->setVisible(tabs.size() >= 2); - break; - - // Always show tabline - case 2: - m_tabline_bar->setVisible(true); - break; - - // Fallback: show tabline for two or more tabs - default: - m_tabline_bar->setVisible(tabs.size() >= 2); - break; - } + return m_shell; } void MainWindow::neovimShowContextMenu() @@ -447,20 +348,6 @@ void MainWindow::neovimSendSelectAll() m_nvim->api0()->vim_command("normal! ggVG"); } -void MainWindow::changeTab(int index) -{ - if (!m_shell->GetShellOptions().IsTablineEnabled()) { - return; - } - - if (m_nvim->api2() == NULL) { - return; - } - - int64_t tab = m_tabline->tabData(index).toInt(); - m_nvim->api2()->nvim_set_current_tabpage(tab); -} - void MainWindow::saveWindowGeometry() { QSettings settings{ "window-geometry" }; diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 96b8049c3..ad1d4082f 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -1,10 +1,8 @@ -#ifndef NEOVIM_QT_MAINWINDOW -#define NEOVIM_QT_MAINWINDOW +#pragma once #include #include #include -#include #include #include @@ -12,6 +10,7 @@ #include "neovimconnector.h" #include "scrollbar.h" #include "shell.h" +#include "tabline.h" #include "treeview.h" namespace NeovimQt { @@ -27,7 +26,7 @@ class MainWindow: public QMainWindow FullScreen, }; - MainWindow(NeovimConnector *, QWidget *parent=0); + MainWindow(NeovimConnector* c, QWidget* parent = nullptr) noexcept; bool isNeovimAttached() const noexcept { return m_shell && m_shell->isNeovimAttached(); } @@ -60,15 +59,11 @@ private slots: void showIfDelayed(); void handleNeovimAttachment(bool); void neovimIsUnsupported(); - void neovimShowtablineSet(int); - void neovimTablineUpdate(int64_t curtab, QList tabs); void neovimShowContextMenu(); void neovimSendCut(); void neovimSendCopy(); void neovimSendPaste(); void neovimSendSelectAll(); - void extTablineSet(bool); - void changeTab(int index); void saveWindowGeometry(); // GuiAdaptive Color/Font/Style Slots @@ -87,8 +82,6 @@ private slots: Shell* m_shell{ nullptr }; DelayedShow m_delayedShow{ DelayedShow::Disabled }; QStackedWidget m_stack; - QTabBar* m_tabline{ nullptr }; - QToolBar* m_tabline_bar{ nullptr }; bool m_neovim_requested_close{ false }; QMenu* m_contextMenu{ nullptr }; @@ -97,6 +90,7 @@ private slots: QAction* m_actPaste{ nullptr }; QAction* m_actSelectAll{ nullptr }; ScrollBar* m_scrollbar{ nullptr }; + Tabline m_tabline; int m_exitStatus{ 0 }; // GuiAdaptive Color/Font/Style @@ -111,6 +105,4 @@ private slots: void updateAdaptiveFont() noexcept; }; -} // Namespace - -#endif +} // namespace NeovimQt diff --git a/src/gui/shell.cpp b/src/gui/shell.cpp index 0b0ad6f17..1e96816c0 100644 --- a/src/gui/shell.cpp +++ b/src/gui/shell.cpp @@ -562,30 +562,8 @@ void Shell::handleRedraw(const QByteArray& name, const QVariantList& opargs) handleBusy(true); } else if (name == "busy_stop"){ handleBusy(false); - } else if (name == "tabline_update") { - if (opargs.size() < 2 || !opargs.at(0).canConvert()) { - qWarning() << "Unexpected argument for tabline_update:" << opargs; - return; - } - int64_t curtab = opargs.at(0).toInt(); - QList tabs; - foreach(const QVariant& tabv, opargs.at(1).toList()) { - QVariantMap tab = tabv.toMap(); - - if (!tab.contains("tab") || !tab.contains("name")) { - qWarning() << "Unexpected tab value in tabline_update:" << tab; - } - - int64_t num = tab.value("tab").toInt(); - QString name = tab.value("name").toString(); - tabs.append(Tab(num, name)); - } - - emit neovimTablineUpdate(curtab, tabs); } else if (name == "option_set") { - if (2 <= opargs.size()) { - handleSetOption(opargs.at(0).toString(), opargs.at(1)); - } + handleSetOption(opargs); } else if (name == "suspend") { if (isWindow()) { setWindowState(windowState() | Qt::WindowMinimized); @@ -942,18 +920,22 @@ void Shell::handleExtGuiOption(const QString& name, const QVariant& value) } } -void Shell::handleSetOption(const QString& name, const QVariant& value) +void Shell::handleSetOption(const QVariantList& opargs) { + if (opargs.size() < 2 || !opargs.at(0).canConvert()) { + qWarning() << "Unexpected arguments for option_set:" << opargs; + return; + } + + const QString name{ opargs.at(0).toString() }; + const QVariant& value{ opargs.at(1) }; + if (name == "guifont") { setGuiFont(value.toString(), false /*force*/); } else if (name == "guifontwide") { handleGuiFontWide(value); } else if (name == "linespace") { handleLineSpace(value); - } else if (name == "showtabline") { - emit neovimShowtablineSet(value.toString().toInt()); - } else if (name == "ext_tabline") { - emit neovimExtTablineSet(value.toBool()); } else { // Uncomment for writing new event handling code. // qDebug() << "Received unknown option" << name << value; diff --git a/src/gui/shell.h b/src/gui/shell.h index 115dcd80d..7f6ede370 100644 --- a/src/gui/shell.h +++ b/src/gui/shell.h @@ -1,16 +1,14 @@ -#ifndef NEOVIM_QT_SHELL -#define NEOVIM_QT_SHELL - -#include -#include -#include +#pragma once #include +#include #include -#include -#include #include #include #include +#include +#include +#include +#include #include "neovimconnector.h" #include "popupmenu.h" @@ -19,20 +17,10 @@ #include "shellwidget/cursor.h" #include "shellwidget/highlight.h" #include "shellwidget/shellwidget.h" +#include "tab.h" namespace NeovimQt { -class Tab { -public: - Tab(int64_t id, QString name) { - this->tab = id; - this->name = name; - } - /// The tab handle, a unique tab identifier - int64_t tab; - QString name; -}; - class Shell: public ShellWidget { Q_OBJECT @@ -95,11 +83,6 @@ class Shell: public ShellWidget void neovimGuiCloseRequest(int status = 0); /// This signal is emmited if the running neovim version is unsupported by the GUI void neovimIsUnsupported(); - void neovimExtTablineSet(bool); - /// The tabline needs updating. curtab is the handle of the current tab (not its index) - /// as seen in Tab::tab. - void neovimTablineUpdate(int64_t curtab, QList tabs); - void neovimShowtablineSet(int); void neovimShowContextMenu(); void colorsChanged(); @@ -159,7 +142,7 @@ protected slots: virtual void handleSetTitle(const QVariantList& opargs); virtual void handleSetScrollRegion(const QVariantList& opargs); virtual void handleBusy(bool); - virtual void handleSetOption(const QString& name, const QVariant& value); + virtual void handleSetOption(const QVariantList& opargs); void handleExtGuiOption(const QString& name, const QVariant& value); virtual void handlePopupMenuShow(const QVariantList& opargs); virtual void handlePopupMenuSelect(const QVariantList& opargs); @@ -288,5 +271,4 @@ template } } -} // Namespace -#endif +} // namespace NeovimQtj diff --git a/src/gui/shelloptions.h b/src/gui/shelloptions.h index a24b233b0..411e28fb1 100644 --- a/src/gui/shelloptions.h +++ b/src/gui/shelloptions.h @@ -2,23 +2,24 @@ namespace NeovimQt { +constexpr bool cs_defaultIsTablineEnabled{ false }; +constexpr bool cs_defaultIsPopupmenuEnabled{ false }; +constexpr bool cs_defaultIsLineGridEnabled{ false }; + class ShellOptions final { public: bool IsTablineEnabled() const noexcept { return m_isTablineEnabled; } bool IsPopupmenuEnabled() const noexcept { return m_isPopupmenuEnabled; } bool IsLineGridEnabled() const noexcept { return m_isLineGridEnabled; } - int GetOptionShowTabline() const noexcept { return m_showtabline; } void SetIsTablineEnabled(bool isEnabled) noexcept { m_isTablineEnabled = isEnabled; } void SetIsPopupmenuEnabled(bool isEnabled) noexcept { m_isPopupmenuEnabled = isEnabled; } void SetIsLineGridEnabled(bool isEnabled) noexcept { m_isLineGridEnabled = isEnabled; } - void SetOptionShowTabline(int value) noexcept { m_showtabline = value; } private: - bool m_isTablineEnabled{ false }; - bool m_isPopupmenuEnabled{ false }; - bool m_isLineGridEnabled{ true }; - int m_showtabline{ 1 /*TwoOrMore*/ }; + bool m_isTablineEnabled{ cs_defaultIsTablineEnabled }; + bool m_isPopupmenuEnabled{ cs_defaultIsPopupmenuEnabled }; + bool m_isLineGridEnabled{ cs_defaultIsLineGridEnabled }; }; } // namespace NeovimQt diff --git a/src/gui/tab.h b/src/gui/tab.h new file mode 100644 index 000000000..ee8f05afd --- /dev/null +++ b/src/gui/tab.h @@ -0,0 +1,23 @@ +#pragma once + +namespace NeovimQt { + +class Tab final +{ +public: + Tab(QString name, uint64_t handle) noexcept + : m_name{ name } + , m_handle{ handle } + { + } + + const QString& GetName() const noexcept { return m_name; } + + uint64_t GetHandle() const noexcept { return m_handle; } + +private: + const QString m_name; + const uint64_t m_handle{}; +}; + +} // namespace NeovimQt diff --git a/src/gui/tabline.cpp b/src/gui/tabline.cpp new file mode 100644 index 000000000..086655897 --- /dev/null +++ b/src/gui/tabline.cpp @@ -0,0 +1,472 @@ +#include "tabline.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "msgpackrequest.h" + +namespace NeovimQt { + +Tabline::Tabline(NeovimConnector& nvim, QWidget* parent) noexcept + : m_nvim{ nvim } + , m_tabline{ this } + , m_bufferline{ this } + , m_spacer{ this } +{ + setAllowedAreas(Qt::TopToolBarArea); + setContextMenuPolicy(Qt::PreventContextMenu); + setFloatable(false); + setMovable(false); + setObjectName("GuiTabline"); + + // Avoid margins around the QTabBar + layout()->setContentsMargins(0, 0, 0, 0); + + auto InititializeQTabBar = [](QTabBar& initTabBar) noexcept + { + initTabBar.setDrawBase(false); + initTabBar.setExpanding(false); + initTabBar.setDocumentMode(true); + initTabBar.setTabsClosable(true); + initTabBar.setFocusPolicy(Qt::NoFocus); + }; + + InititializeQTabBar(m_tabline); + InititializeQTabBar(m_bufferline); + + // Spacer between Tabs + Buffers + m_spacer.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + m_tablineAction = addWidget(&m_tabline); + m_spacerAction = addWidget(&m_spacer); + m_bufferlineAction = addWidget(&m_bufferline); + + connect(&m_nvim, &NeovimConnector::ready, this, &Tabline::neovimConnectorReady); + connect(&m_tabline, &QTabBar::currentChanged, this, &Tabline::currentChangedTabline); + connect(&m_tabline, &QTabBar::tabCloseRequested, this, &Tabline::closeRequestedTabline); + connect(&m_bufferline, &QTabBar::currentChanged, this, &Tabline::currentChangedBufline); + connect(&m_bufferline, &QTabBar::tabCloseRequested, this, &Tabline::closeRequestedBufline); + + QSettings settings; + m_isEnabled = settings.value("ext_tabline", cs_defaultIsTablineEnabled).toBool(); + updateTablineVisibility(); +} + +void Tabline::neovimConnectorReady() noexcept +{ + connect( + m_nvim.api0(), &NeovimApi0::neovimNotification, this, &Tabline::handleNeovimNotification); + m_nvim.api0()->vim_subscribe("Gui"); +} + +void Tabline::handleNeovimNotification(const QByteArray& name, const QVariantList& args) noexcept +{ + if (name == "Gui") { + handleGuiOption(args); + return; + } + + if (name == "redraw") { + Shell::DispatchRedrawNotifications(this, args); + return; + } +} + +void Tabline::handleRedraw(const QByteArray& name, const QVariantList& args) noexcept +{ + if (name == "tabline_update") { + handleTablineUpdate(args); + return; + } + + if (name == "option_set") { + handleOptionShowTabline(args); + } +} + +void Tabline::handleGuiOption(const QVariantList& args) noexcept +{ + if (args.size() < 2 || !args.at(0).canConvert() || !args.at(1).canConvert()) { + return; + } + + const QString guiEventName{ args.at(0).toString() }; + + if (guiEventName != "Option") { + return; + } + + const QString option{ args.at(1).toString() }; + + if (option == "Tabline") { + handleGuiTabline(args); + } +} + +void Tabline::handleGuiTabline(const QVariantList& args) noexcept +{ + if (args.size() < 3 || !args.at(2).canConvert()) { + qWarning() << "Unexpected format for GuiTabline:" << args; + return; + } + + const bool isEnabled{ args.at(2).toBool() }; + m_isEnabled = isEnabled; + updateTablineVisibility(); +} + +static std::vector ParseTablineVariant(const QVariantList tabs) noexcept +{ + std::vector tabList; + + for (const auto& varTab : tabs) { + if (static_cast(varTab.type()) != QMetaType::QVariantMap) { + qWarning() << "Unexpected varTab value in tabline_update:" << varTab; + continue; + } + + const QVariantMap tabMap = varTab.toMap(); + const QString key{ (tabMap.contains("tab")) ? "tab" : "buffer" }; + + if (!tabMap.contains(key) || !tabMap.contains("name")) { + qWarning() << "Unexpected tabMap value in tabline_update:" << tabMap; + continue; + } + + const uint64_t tab{ tabMap.value(key).toULongLong() }; + const QString name{ tabMap.value("name").toString() }; + tabList.emplace_back(name, tab); + } + + return tabList; +} + +void Tabline::handleTablineUpdate(const QVariantList& args) noexcept +{ + if (args.size() < 2 || !args.at(0).canConvert() + || static_cast(args.at(1).type()) != QMetaType::QVariantList) { + qWarning() << "Unexpected argument for tabline_update:" << args; + return; + } + + const uint64_t curtab{ args.at(0).toULongLong() }; + const QVariantList tabs = args.at(1).toList(); + + const std::vector tabList{ ParseTablineVariant(tabs) }; + + if (args.size() < 4) { + drawTablineUpdates(tabList, curtab, {}, 0); + return; + } + + if (!args.at(2).canConvert() + || static_cast(args.at(3).type()) != QMetaType::QVariantList) { + qWarning() << "Unexpected argument for tabline_update:" << args; + return; + } + + const uint64_t curbuf{ args.at(2).toULongLong() }; + const QVariantList buffers = args.at(3).toList(); + + const std::vector bufferList{ ParseTablineVariant(buffers) }; + + drawTablineUpdates(tabList, curtab, bufferList, curbuf); +} + +void Tabline::handleOptionShowTabline(const QVariantList& args) noexcept +{ + if (args.size() < 1 || !args.at(0).canConvert()) { + return; + } + + const QString optionName{ args.at(0).toString() }; + + if (optionName != "showtabline") { + return; + } + + if (args.size() < 2 || !args.at(1).canConvert()) { + qWarning() << "Tabline unexpected format for option showtabline:" << args; + } + + const int value{ args.at(1).toInt() }; + + auto OptionFromInteger = [](int value) noexcept -> OptionShowTabline + { + const OptionShowTabline enumValue{ static_cast(value) }; + + switch (enumValue) { + case OptionShowTabline::Never: + case OptionShowTabline::AtLeastTwo: + case OptionShowTabline::Always: + return enumValue; + } + + // Error: Unrecognized value, fallback to default value 1 (AtLeastTwo) + qWarning() << QStringLiteral("Error: unrecognized value for showtabline { %1 }").arg(value); + return OptionShowTabline::AtLeastTwo; + }; + + m_optionShowTabline = OptionFromInteger(value); + updateTablineVisibility(); +} + +static QIcon GetIconFromFilePath(const QString& path) noexcept +{ + static std::mutex s_iconCacheLock; + + static QFileIconProvider s_iconProvider; + + using CachedBufferIcon = std::pair; + static std::vector s_iconCache; + + { + std::lock_guard guard(s_iconCacheLock); + + auto pathMatchesExact = [&](const CachedBufferIcon& cacheEntry) noexcept + { + // We assume paths are case sensitive strings, this is not always correct. + // False positives may result in duplicate entries, this is okay. + // Examples: Windows paths, Windows short vs long paths, etc + return cacheEntry.first == path; + }; + + auto result{ std::find_if(s_iconCache.begin(), s_iconCache.end(), pathMatchesExact) }; + + // Use cached icon if present, avoids repeated disk I/O + if (result != s_iconCache.end()) { + return result->second; + } + + // No icon exists for the path, create one + QIcon icon{ s_iconProvider.icon(QFileInfo{ path }) }; + + // Some file-does-not-exist cases display strange icons, use generic text icon + const QString iconName{ icon.name() }; + if (iconName == "unknown" || iconName == "application-octet-stream") { + icon = QIcon::fromTheme("text-x-generic"); + } + + s_iconCache.emplace_back(path, icon); + return icon; + } +} + +static void SetTabIconAndTooltipCallback( + QPointer bufferline, int bufIndex, const QVariant& resp) noexcept +{ + if (!resp.canConvert()) { + qWarning() << "Unexpected buffer path format in drawTablineUpdates"; + return; + } + + if (!bufferline) { + return; + } + + const QString bufferPath{ resp.toString() }; + + bufferline->setTabToolTip(bufIndex, bufferPath); + bufferline->setTabIcon(bufIndex, GetIconFromFilePath(bufferPath)); +} + +void Tabline::drawTablineUpdates( + const std::vector tabList, + uint64_t curtab, + const std::vector& bufferList, + uint64_t curbuf) noexcept +{ + updateTabControl(m_tabline, m_nvim.api0(), tabList, curtab, false /*drawTabIcons*/); + updateTabControl(m_bufferline, m_nvim.api0(), bufferList, curbuf, true /*drawTabIcons*/); + updateTablineVisibility(); +} + +void Tabline::updateTabControl( + QTabBar& tabControl, + NeovimApi0* nvimApi0, + const std::vector tabList, + uint64_t curtab, + bool drawTabIcons) noexcept +{ + // Remove closed/deleted tabs + for (int i = tabList.size(); i < tabControl.count(); i++) { + tabControl.removeTab(i); + } + + int tabIndex{ 0 }; + for (const auto& tab : tabList) { + // Required: Set Tab Text + QString text{ tab.GetName() }; + + // Escape & in tab name otherwise it will be interpreted as + // a keyboard shortcut (#357) - escaping is done using && + text.replace("&", "&&"); + + if (tabControl.count() <= tabIndex) { + tabControl.addTab(text); + } + else { + tabControl.setTabText(tabIndex, text); + } + + // Required: Set Tab Neovim Handle + tabControl.setTabData(tabIndex, QVariant::fromValue(tab.GetHandle())); + + // Optional: Mark active tab + if (curtab == tab.GetHandle()) { + tabControl.setCurrentIndex(tabIndex); + } + + // Optional: Add filetype icons + if (drawTabIcons && nvimApi0) { + auto reqBufferPath{ nvimApi0->vim_eval( + QStringLiteral("expand('#%1:p')").arg(tab.GetHandle()).toLatin1()) }; + + QPointer spTabControl{ &tabControl }; + auto handle = [spTabControl, tabIndex](quint32, quint64, const QVariant& resp) noexcept + { + SetTabIconAndTooltipCallback(spTabControl, tabIndex, resp); + }; + connect(reqBufferPath, &MsgpackRequest::finished, this, handle); + } + + tabIndex++; + } +} + +void Tabline::updateTablineVisibility() noexcept +{ + if (!m_isEnabled) { + setVisible(false); + return; + } + + if (!m_tablineAction || !m_bufferlineAction) { + qWarning() << "Tabline is missing Buffer/Tab QAction!"; + return; + } + + // Legacy Mode: Neovim does not provide buffer info to GuiTabline + // Support for tabs + buffers was added in Neovim API 8 + const bool isLegacyMode{ m_bufferline.count() == 0 }; + + const bool isAtLeastTwo{ m_tabline.count() >= 2 }; + + switch (m_optionShowTabline) { + case OptionShowTabline::Never: + setVisible(false); + m_bufferlineAction->setVisible(false); + m_spacerAction->setVisible(false); + m_tablineAction->setVisible(false); + break; + + case OptionShowTabline::AtLeastTwo: + setVisible(isAtLeastTwo); + m_bufferlineAction->setVisible(!isLegacyMode && isAtLeastTwo); + m_spacerAction->setVisible(!isLegacyMode && isAtLeastTwo); + m_tablineAction->setVisible(isAtLeastTwo); + break; + + // Users expect buffers to appear as tabs, similar to vim-airline. + // When no vim-tabs are present, we display the vim-buffers on the left. + // Once two or more vim-tabs are present, we display vim-tabs on the left + // and vim-buffers on the right; similar behavior to vim-airline. + case OptionShowTabline::Always: + setVisible(true); + m_bufferlineAction->setVisible(!isLegacyMode && true); + m_spacerAction->setVisible(!isLegacyMode && isAtLeastTwo); + m_tablineAction->setVisible(isAtLeastTwo || isLegacyMode); + break; + } +} + +void Tabline::currentChangedTabline(int index) noexcept +{ + if (!m_nvim.api0()) { + return; + } + + const uint64_t handle{ m_tabline.tabData(index).toULongLong() }; + + m_nvim.api0()->vim_set_current_tabpage(handle); +} + +void Tabline::closeRequestedTabline(int index) noexcept +{ + if (!m_nvim.api0()) { + return; + } + + const uint64_t handle{ m_bufferline.tabData(index).toULongLong() }; + m_nvim.api0()->vim_command(QStringLiteral("tabclose %1").arg(handle).toLatin1()); +} + +void Tabline::currentChangedBufline(int index) noexcept +{ + if (!m_nvim.api0()) { + return; + } + + const uint64_t handle{ m_bufferline.tabData(index).toULongLong() }; + m_nvim.api0()->vim_command(QStringLiteral("buffer! %1").arg(handle).toLatin1()); +} + +static QString GetSanitizedErrorString(const QVariant& err) noexcept +{ + static const QString s_errorUnknown{ + "Unknown error closing buffer!\nPlease save and try again." + }; + static const QString s_errorUnsaved{ + "No write since last change!\nPlease save and try again." + }; + + if (err.type() != QVariant::Type::List) { + return s_errorUnknown; + } + + const QVariantList errList = err.toList(); + if (errList.size() < 2 || !errList.at(1).canConvert()) { + return s_errorUnknown; + } + + const QString errorText{ errList.at(1).toString() }; + + static const QRegularExpression s_reUnsaved{ + "^.*E89: No write since last change for buffer (\\d+) \\(add \\! to override\\)$" + }; + if (s_reUnsaved.match(errorText).hasMatch()) { + return s_errorUnsaved; + } + + return s_errorUnknown; +} + +void Tabline::handleCloseBufferError(quint32 msgid, quint64 fun, const QVariant& err) noexcept +{ + QMessageBox msgBox; + msgBox.setText(GetSanitizedErrorString(err)); + msgBox.setIcon(QMessageBox::Icon::Warning); + msgBox.exec(); +} + +void Tabline::closeRequestedBufline(int index) noexcept +{ + if (!m_nvim.api0()) { + return; + } + + const uint64_t handle{ m_bufferline.tabData(index).toULongLong() }; + + auto reqCloseBuffer{ m_nvim.api0()->vim_command( + QStringLiteral("bdel %1").arg(handle).toLatin1()) }; + connect(reqCloseBuffer, &MsgpackRequest::error, this, &Tabline::handleCloseBufferError); +} + +} // namespace NeovimQt diff --git a/src/gui/tabline.h b/src/gui/tabline.h new file mode 100644 index 000000000..143b3917f --- /dev/null +++ b/src/gui/tabline.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include "neovimconnector.h" +#include "shell.h" +#include "shelloptions.h" +#include "tab.h" + +namespace NeovimQt { + +class Tabline : public QToolBar +{ + Q_OBJECT + +public: + Tabline(NeovimConnector& nvim, QWidget* parent) noexcept; + + void handleNeovimNotification(const QByteArray& name, const QVariantList& args) noexcept; + void handleRedraw(const QByteArray& name, const QVariantList& opargs) noexcept; + +private slots: + void currentChangedTabline(int index) noexcept; + void closeRequestedTabline(int index) noexcept; + void currentChangedBufline(int index) noexcept; + void closeRequestedBufline(int index) noexcept; + +private: + void neovimConnectorReady() noexcept; + + void handleGuiOption(const QVariantList& args) noexcept; + void handleGuiTabline(const QVariantList& args) noexcept; + void handleTablineUpdate(const QVariantList& args) noexcept; + void handleOptionShowTabline(const QVariantList& args) noexcept; + void handleCloseBufferError(quint32 msgid, quint64 fun, const QVariant& err) noexcept; + + void drawTablineUpdates( + const std::vector tabList, + uint64_t curtab, + const std::vector& bufferList, + uint64_t curbuf) noexcept; + + void updateTabControl( + QTabBar& tabControl, + NeovimApi0* nvimApi0, + const std::vector tabList, + uint64_t curtab, + bool drawTabIcons) noexcept; + + void updateTablineVisibility() noexcept; + void updateTablineVisibilityLegacyMode() noexcept; + void updateTablinePathCallback() noexcept; + + enum class OptionShowTabline : int + { + Never = 0, + AtLeastTwo = 1, + Always = 2, + }; + + NeovimConnector& m_nvim; + bool m_isEnabled{ cs_defaultIsTablineEnabled }; + + QTabBar m_tabline; + QAction* m_tablineAction{}; + + QTabBar m_bufferline; + QAction* m_bufferlineAction{}; + + QWidget m_spacer; + QAction* m_spacerAction{}; + + OptionShowTabline m_optionShowTabline{ OptionShowTabline::AtLeastTwo }; +}; + +} // namespace NeovimQt diff --git a/test/tst_shell.cpp b/test/tst_shell.cpp index fcf23d6e7..827fc78f8 100644 --- a/test/tst_shell.cpp +++ b/test/tst_shell.cpp @@ -86,16 +86,6 @@ private slots: checkStartVars(c); } - void guiExtTablineSet() { - QStringList args; - args << "-u" << "NONE"; - NeovimConnector *c = NeovimConnector::spawn(args); - Shell *s = new Shell(c); - QSignalSpy onOptionSet(s, &Shell::neovimExtTablineSet); - QVERIFY(onOptionSet.isValid()); - QVERIFY(SPYWAIT(onOptionSet)); - } - void gviminit() { qputenv("GVIMINIT", "let g:test_gviminit = 1"); QStringList args; @@ -184,10 +174,6 @@ private slots: #else QCOMPARE(s->shell()->fontDesc(), QString("DejaVu Sans Mono:h14:l")); #endif - - // GuiTabline - QSignalSpy onOptionSet(s->shell(), &Shell::neovimExtTablineSet); - QVERIFY(onOptionSet.isValid()); } void CloseEvent_data() {