From 2f199aa04dcb8a39fb7c3097a1e2bde83c1b9930 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Thu, 5 Feb 2026 23:11:12 +0530 Subject: [PATCH] feat: Making macos traffic buttons to be overlayed on top of app and alligning app for this to look okay --- README.md | 6 +- app/CMakeLists.txt | 16 +- app/Readme.md | 2 +- app/icons/trafficlights/close.png | Bin 0 -> 292 bytes app/icons/trafficlights/maximize.png | Bin 0 -> 223 bytes app/icons/trafficlights/minimise.png | Bin 0 -> 130 bytes app/macos/Info.plist.in | 4 +- app/macos/macWindowStyle.h | 8 + app/macos/macWindowStyle.mm | 25 ++ app/macos/trafficLightsTitleBar.cpp | 263 ++++++++++++++++++ app/macos/trafficLightsTitleBar.h | 48 ++++ app/resources.qrc | 3 + app/window.cpp | 89 +++++- app/window.h | 14 +- flake.nix | 4 +- logos_dapps/main_ui/src/MainContainer.cpp | 8 +- logos_dapps/main_ui/src/mdiview.cpp | 135 +++++---- logos_dapps/main_ui/src/mdiview.h | 5 +- .../src/qml/controls/SidebarCircleButton.qml | 4 +- .../controls/SidebarCircleButtonContainer.qml | 2 +- .../main_ui/src/qml/panels/SidebarPanel.qml | 9 +- nix/app.nix | 31 ++- nix/appimage.nix | 6 +- nix/default.nix | 4 +- run-dev.sh | 10 +- 25 files changed, 583 insertions(+), 113 deletions(-) create mode 100644 app/icons/trafficlights/close.png create mode 100644 app/icons/trafficlights/maximize.png create mode 100644 app/icons/trafficlights/minimise.png create mode 100644 app/macos/macWindowStyle.h create mode 100644 app/macos/macWindowStyle.mm create mode 100644 app/macos/trafficLightsTitleBar.cpp create mode 100644 app/macos/trafficLightsTitleBar.h diff --git a/README.md b/README.md index 146b067..4729cb8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# logos-app-poc +# logos-app ## How to Build @@ -15,7 +15,7 @@ nix build '.#default' ``` The result will include: -- `/bin/logos-app-poc` - The main Logos application executable +- `/bin/logos-app` - The main Logos application executable - All required modules and dependencies #### Build Individual Components @@ -48,7 +48,7 @@ After building with `nix build`, you can run the application: ```bash # Run the main Logos application -./result/bin/logos-app-poc +./result/bin/logos-app ``` The application will automatically load all required modules and dependencies. All components are bundled in the Nix store layout. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e251b83..26b6f0c 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(LogosApp LANGUAGES CXX) +project(LogosApp LANGUAGES CXX OBJCXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -19,9 +19,10 @@ endif() find_package(Qt6 COMPONENTS Widgets REQUIRED) -# Add interfaces directory to include path +# Add interfaces and macos directories to include path include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/interfaces) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/macos) # Allow override from environment or command line (for nix builds) if(NOT DEFINED LOGOS_LIBLOGOS_ROOT) @@ -84,10 +85,17 @@ set(PROJECT_SOURCES main.cpp window.h window.cpp + macos/trafficLightsTitleBar.h + macos/trafficLightsTitleBar.cpp + macos/macWindowStyle.h interfaces/IComponent.h resources.qrc ) +if(APPLE) + list(APPEND PROJECT_SOURCES macos/macWindowStyle.mm) +endif() + qt_add_executable(LogosApp ${PROJECT_SOURCES} ) @@ -98,6 +106,10 @@ target_link_libraries(LogosApp PRIVATE logos_core ) +if(APPLE) + target_link_libraries(LogosApp PRIVATE "-framework Cocoa" "-framework QuartzCore") +endif() + # Copy the core library to the app's lib directory and fix its install name if(APPLE) if(_liblogos_is_source) diff --git a/app/Readme.md b/app/Readme.md index 652c1a3..a7e0821 100644 --- a/app/Readme.md +++ b/app/Readme.md @@ -1,4 +1,4 @@ -# Logos Core POC +# Logos App ## Building the Application diff --git a/app/icons/trafficlights/close.png b/app/icons/trafficlights/close.png new file mode 100644 index 0000000000000000000000000000000000000000..578604c01ad19ba06e33253d19b68aca2cca51dc GIT binary patch literal 292 zcmV+<0o(qGP)|{P@7}}IszMX1V-ryi~#sjMZJ90CXx@0+V_0OuM>1- z;>+0ataS@=432dt>W3(&bz>wsXOd4-i&c`Nl$LW1Dg8EiY6U=o;cazN!$^G92Q0zx zZ4Gpog06w&2_(DWIr6*8`mI5N;6(lAbu6*K4U!#ji=+@gTyQMGaEAoLJrWG}#^05A z495~b+%Lf)Dv@WG1cwNR(G1^{cr)A}F~E_eooG5TZE!5nVW!IGEghqHsaS^Y-NO-337Uv%rM{jtmPH6B#BcK1Sa5 zhAwA^KYmjY$;gS zB>B+k^UKEN?Of_U{Q?0E)(jdI@*V##OWtRXwzG9Pz^LR<&%?vY&Zc0vfUm)=@d1NN dqk;t&L*iWJL;0*}uYra#c)I$ztaD0e0s!ILCFuYF literal 0 HcmV?d00001 diff --git a/app/macos/Info.plist.in b/app/macos/Info.plist.in index 3a1f80f..0fcbad2 100644 --- a/app/macos/Info.plist.in +++ b/app/macos/Info.plist.in @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - LogosApp + Logos App CFBundleExecutable LogosApp CFBundleIconFile @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - LogosApp + Logos App CFBundlePackageType APPL CFBundleShortVersionString diff --git a/app/macos/macWindowStyle.h b/app/macos/macWindowStyle.h new file mode 100644 index 0000000..b56ed49 --- /dev/null +++ b/app/macos/macWindowStyle.h @@ -0,0 +1,8 @@ +#ifndef MACWINDOWSTYLE_H +#define MACWINDOWSTYLE_H + +class QMainWindow; + +void applyMacWindowRoundedCorners(QMainWindow* w, bool rounded = true); + +#endif // MACWINDOWSTYLE_H diff --git a/app/macos/macWindowStyle.mm b/app/macos/macWindowStyle.mm new file mode 100644 index 0000000..4152fe2 --- /dev/null +++ b/app/macos/macWindowStyle.mm @@ -0,0 +1,25 @@ +#include +#ifdef Q_OS_MAC +#import +#import +#endif + +#include "macWindowStyle.h" +#include + +void applyMacWindowRoundedCorners(QMainWindow* w, bool rounded) +{ +#ifdef Q_OS_MAC + if (!w) return; + NSView* nsView = (NSView*)w->winId(); + if (!nsView) return; + + nsView.wantsLayer = YES; + if (nsView.layer) { + nsView.layer.cornerRadius = rounded ? 10.0 : 0.0; + nsView.layer.masksToBounds = rounded; + nsView.layer.borderWidth = rounded ? 0.5 : 0.0; + nsView.layer.borderColor = rounded ? [NSColor separatorColor].CGColor : nullptr; + } +#endif +} diff --git a/app/macos/trafficLightsTitleBar.cpp b/app/macos/trafficLightsTitleBar.cpp new file mode 100644 index 0000000..852a6c9 --- /dev/null +++ b/app/macos/trafficLightsTitleBar.cpp @@ -0,0 +1,263 @@ +#include "trafficLightsTitleBar.h" +#include +#include +#include +#include +#include +#include +#include + +QPushButton* makeTrafficButton(const QString& colorHex, const QString& borderHex, int size, QWidget* parent) { + auto* btn = new QPushButton(parent); + btn->setFixedSize(size, size); + btn->setCursor(Qt::ArrowCursor); + btn->setFlat(true); + btn->setStyleSheet( + QString("QPushButton {" + " background-color: %1;" + " border: 0.5px solid %2;" + " border-radius: %3px;" + "}" + "QPushButton:hover { background-color: %1; }" + "QPushButton:pressed { background-color: %1; }") + .arg(colorHex, borderHex) + .arg(size / 2)); + return btn; +} + +TrafficLightsTitleBar::TrafficLightsTitleBar(QWidget* parent) : QWidget(parent) { + setFixedHeight(kTitleBarHeight); + setCursor(Qt::ArrowCursor); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_NoSystemBackground); + setAutoFillBackground(false); + + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(kLeftMargin, kTopMargin, 0, 0); + layout->setSpacing(kButtonSpacing); + + m_closeBtn = makeTrafficButton("#FF5F57", "#E0443E", kButtonSize, this); + layout->addWidget(m_closeBtn); + + m_minBtn = makeTrafficButton("#FEBC2E", "#DE9E24", kButtonSize, this); + layout->addWidget(m_minBtn); + + m_zoomBtn = makeTrafficButton("#28C840", "#1AAC37", kButtonSize, this); + layout->addWidget(m_zoomBtn); + + layout->addStretch(1); + + connect(m_closeBtn, &QPushButton::clicked, this, [this]() { window()->hide(); }); + connect(m_minBtn, &QPushButton::clicked, this, [this]() { window()->showMinimized(); }); + connect(m_zoomBtn, &QPushButton::clicked, this, [this]() { + QWidget* w = window(); + if (w->windowState() & Qt::WindowFullScreen) + w->setWindowState(Qt::WindowNoState); + else + w->setWindowState(Qt::WindowFullScreen); + }); + + m_closeBtn->installEventFilter(this); + m_minBtn->installEventFilter(this); + m_zoomBtn->installEventFilter(this); +} + +bool TrafficLightsTitleBar::isOverButton(const QPoint& pos) const { + if (m_closeBtn && m_closeBtn->isVisible() && m_closeBtn->geometry().contains(pos)) { + return true; + } + if (m_minBtn && m_minBtn->isVisible() && m_minBtn->geometry().contains(pos)) { + return true; + } + if (m_zoomBtn && m_zoomBtn->isVisible() && m_zoomBtn->geometry().contains(pos)) { + return true; + } + return false; +} + +void TrafficLightsTitleBar::setButtonIcon(QPushButton* btn, const QString& iconPath) { + if (!btn) return; + const QSize iconSize(6, 6); + btn->setIcon(QIcon(iconPath)); + btn->setIconSize(iconSize); +} + +void TrafficLightsTitleBar::setAllButtonIcons() { + setButtonIcon(m_closeBtn, ":/icons/trafficlights/close.png"); + setButtonIcon(m_minBtn, ":/icons/trafficlights/minimise.png"); + setButtonIcon(m_zoomBtn, ":/icons/trafficlights/maximize.png"); +} + +void TrafficLightsTitleBar::clearAllButtonIcons() { + if (m_closeBtn) m_closeBtn->setIcon(QIcon()); + if (m_minBtn) m_minBtn->setIcon(QIcon()); + if (m_zoomBtn) m_zoomBtn->setIcon(QIcon()); +} + +void TrafficLightsTitleBar::leaveEvent(QEvent* e) { + QWidget::leaveEvent(e); + clearAllButtonIcons(); +} + +bool TrafficLightsTitleBar::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::Enter) { + if (watched == m_closeBtn || watched == m_minBtn || watched == m_zoomBtn) { + setAllButtonIcons(); + } + } else if (event->type() == QEvent::Leave) { + if (watched == m_closeBtn || watched == m_minBtn || watched == m_zoomBtn) { + clearAllButtonIcons(); + } + } + return QWidget::eventFilter(watched, event); +} + +void TrafficLightsTitleBar::forwardEventToCentralWidget(QMouseEvent* e) { + QMainWindow* mainWindow = qobject_cast(window()); + if (!mainWindow) return; + + QWidget* centralWidget = mainWindow->centralWidget(); + if (!centralWidget) return; + + // Map coordinates from title bar space to central widget space + QPoint globalPos = mapToGlobal(e->pos()); + QPoint centralPos = centralWidget->mapFromGlobal(globalPos); + + // Check if the position is within central widget bounds + if (!centralWidget->rect().contains(centralPos)) return; + + // Find the actual child widget at this position + QWidget* targetWidget = centralWidget->childAt(centralPos); + if (!targetWidget) { + // No specific child at this position, send to central widget itself + targetWidget = centralWidget; + } + + // Map coordinates to target widget's space + QPoint targetPos = targetWidget->mapFromGlobal(globalPos); + + // Create a new event with mapped coordinates + QMouseEvent mappedEvent( + e->type(), + targetPos, + e->globalPosition(), + e->button(), + e->buttons(), + e->modifiers() + ); + + // Send the event to the target widget + QApplication::sendEvent(targetWidget, &mappedEvent); +} + +void TrafficLightsTitleBar::mousePressEvent(QMouseEvent* e) { + if (isOverButton(e->pos())) { + QWidget::mousePressEvent(e); + return; + } + + // Only handle left button for potential dragging + // Forward all other buttons (right-click, middle-click, etc.) to central widget + if (e->button() != Qt::LeftButton) { + forwardEventToCentralWidget(e); + e->ignore(); + return; + } + + // Left button: prepare for potential drag + m_dragActive = false; + m_dragStartPos = e->pos(); + e->accept(); +} + +void TrafficLightsTitleBar::mouseMoveEvent(QMouseEvent* e) { + if (isOverButton(e->pos())) { + QWidget::mouseMoveEvent(e); + return; + } + + // If left button is pressed and we haven't started dragging yet + if (!m_dragActive && (e->buttons() & Qt::LeftButton)) { + // Check if we've moved enough to start a drag + int distance = (e->pos() - m_dragStartPos).manhattanLength(); + if (distance >= kDragThresholdPx) { + if (QWindow* w = window()->windowHandle()) { + w->startSystemMove(); + m_dragActive = true; + } + } + } else if (!(e->buttons() & Qt::LeftButton)) { + // No left button - forward hover events to central widget + forwardEventToCentralWidget(e); + } + + QWidget::mouseMoveEvent(e); +} + +void TrafficLightsTitleBar::mouseReleaseEvent(QMouseEvent* e) { + if (isOverButton(e->pos())) { + QWidget::mouseReleaseEvent(e); + return; + } + + // If this was a left button release and we never started dragging, + // it was a click - forward it to the central widget + if (e->button() == Qt::LeftButton) { + bool wasClick = !m_dragActive; + m_dragActive = false; + + if (wasClick) { + // This was a click, not a drag - forward the release to central widget + // Also synthesize a press event so the central widget gets a complete click + QMainWindow* mainWindow = qobject_cast(window()); + if (mainWindow) { + QWidget* centralWidget = mainWindow->centralWidget(); + if (centralWidget) { + QPoint globalPos = mapToGlobal(e->pos()); + QPoint centralPos = centralWidget->mapFromGlobal(globalPos); + + if (centralWidget->rect().contains(centralPos)) { + // Find the actual child widget at this position + QWidget* targetWidget = centralWidget->childAt(centralPos); + if (!targetWidget) { + targetWidget = centralWidget; + } + + // Map coordinates to target widget's space + QPoint targetPos = targetWidget->mapFromGlobal(globalPos); + + // Send press event + QMouseEvent pressEvent( + QEvent::MouseButtonPress, + targetPos, + e->globalPosition(), + e->button(), + e->button(), + e->modifiers() + ); + QApplication::sendEvent(targetWidget, &pressEvent); + + forwardEventToCentralWidget(e); + } + } + } + } + } else { + // Non-left button release - forward to central widget + forwardEventToCentralWidget(e); + } +} + +void TrafficLightsTitleBar::mouseDoubleClickEvent(QMouseEvent* e) { + if (isOverButton(e->pos())) { + QWidget::mouseDoubleClickEvent(e); + return; + } + + forwardEventToCentralWidget(e); + e->ignore(); +} \ No newline at end of file diff --git a/app/macos/trafficLightsTitleBar.h b/app/macos/trafficLightsTitleBar.h new file mode 100644 index 0000000..441f3b1 --- /dev/null +++ b/app/macos/trafficLightsTitleBar.h @@ -0,0 +1,48 @@ +#ifndef TRAFFICLIGHTSTITLEBAR_H +#define TRAFFICLIGHTSTITLEBAR_H + +#include +#include +#include + +class QPushButton; +class QMouseEvent; + +class TrafficLightsTitleBar : public QWidget +{ + Q_OBJECT + +public: + static const int kTitleBarHeight = 28; + static const int kButtonSize = 12; + static const int kButtonSpacing = 6; + static const int kLeftMargin = 10; + static const int kTopMargin = 4; + + explicit TrafficLightsTitleBar(QWidget* parent = nullptr); + +protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseDoubleClickEvent(QMouseEvent* e) override; + void leaveEvent(QEvent* e) override; + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + bool isOverButton(const QPoint& pos) const; + void forwardEventToCentralWidget(QMouseEvent* e); + void setButtonIcon(QPushButton* btn, const QString& iconPath); + void setAllButtonIcons(); + void clearAllButtonIcons(); + + QPushButton* m_closeBtn = nullptr; + QPushButton* m_minBtn = nullptr; + QPushButton* m_zoomBtn = nullptr; + + bool m_dragActive = false; + QPoint m_dragStartPos; + static const int kDragThresholdPx = 4; +}; + +#endif // TRAFFICLIGHTSTITLEBAR_H diff --git a/app/resources.qrc b/app/resources.qrc index fed4c6d..5a47c1c 100644 --- a/app/resources.qrc +++ b/app/resources.qrc @@ -2,5 +2,8 @@ icons/logos.png + icons/trafficlights/close.png + icons/trafficlights/minimise.png + icons/trafficlights/maximize.png \ No newline at end of file diff --git a/app/window.cpp b/app/window.cpp index 26b24a1..3fb1fa1 100644 --- a/app/window.cpp +++ b/app/window.cpp @@ -13,6 +13,11 @@ #include #include #include +#include +#ifdef Q_OS_MAC + #include "trafficLightsTitleBar.h" + #include "macWindowStyle.h" +#endif Window::Window(QWidget *parent) : QMainWindow(parent) @@ -103,7 +108,6 @@ void Window::setupUi() if (mainContent) { setCentralWidget(mainContent); - // Pass the package manager widget to main_ui if it was loaded if (packageManagerWidget && mainUiPlugin) { QMetaObject::invokeMethod(mainUiPlugin, "setPackageManagerWidget", @@ -127,15 +131,79 @@ void Window::setupUi() layout->addWidget(messageLabel); setCentralWidget(fallbackWidget); - qWarning() << "Failed to load main UI plugin from:" << mainUiPluginPath; } - // Set window title and size - setWindowTitle("Logos Core POC"); resize(1024, 768); + +#ifdef Q_OS_MAC + setWindowFlags(windowFlags() | Qt::FramelessWindowHint); + setupMacOSDockReopen(); + // Create title bar after resize() so it gets full width from the start + m_trafficLightsTitleBar = new TrafficLightsTitleBar(this); + m_trafficLightsTitleBar->setGeometry(0, 0, width(), TrafficLightsTitleBar::kTitleBarHeight); + m_trafficLightsTitleBar->show(); + m_trafficLightsTitleBar->raise(); +#endif } +void Window::changeEvent(QEvent* event) +{ + QMainWindow::changeEvent(event); +#ifdef Q_OS_MAC + if (event->type() == QEvent::WindowStateChange) { + const bool fullScreen = (windowState() & Qt::WindowFullScreen) != 0; + if (m_trafficLightsTitleBar) { + if (fullScreen) + m_trafficLightsTitleBar->hide(); + else + m_trafficLightsTitleBar->show(); + } + applyMacWindowRoundedCorners(this, !fullScreen); + // This is needed to fix squared corners after exiting fullscreen mode + if (!fullScreen) { + const int w = width(); + const int h = height(); + resize(w - 1, h - 1); + QTimer::singleShot(0, this, [this, w, h]() { + resize(w, h); + applyMacWindowRoundedCorners(this, true); + }); + } + } +#endif +} + +void Window::resizeEvent(QResizeEvent* event) +{ + QMainWindow::resizeEvent(event); +#ifdef Q_OS_MAC + if (m_trafficLightsTitleBar && m_trafficLightsTitleBar->isVisible()) + m_trafficLightsTitleBar->setGeometry(0, 0, width(), TrafficLightsTitleBar::kTitleBarHeight); +#endif +} + +void Window::showEvent(QShowEvent* event) +{ + QMainWindow::showEvent(event); +#ifdef Q_OS_MAC + applyMacWindowRoundedCorners(this); +#endif +} + +#ifdef Q_OS_MAC +void Window::setupMacOSDockReopen() +{ + connect(qApp, &QApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) { + if (state == Qt::ApplicationActive && !isVisible()) { + show(); + raise(); + activateWindow(); + } + }); +} +#endif + void Window::createTrayIcon() { if (!QSystemTrayIcon::isSystemTrayAvailable()) { @@ -146,7 +214,7 @@ void Window::createTrayIcon() // Create tray icon m_trayIcon = new QSystemTrayIcon(this); setIcon(); - m_trayIcon->setToolTip("Logos Core POC"); + m_trayIcon->setToolTip("Logos App"); // Create context menu m_trayIconMenu = new QMenu(this); @@ -192,6 +260,15 @@ void Window::setIcon() void Window::closeEvent(QCloseEvent *event) { +#ifdef Q_OS_MAC + // In full screen, close only exits full screen (Discord-style); do not hide + if (windowState() & Qt::WindowFullScreen) { + setWindowState(Qt::WindowNoState); + event->ignore(); + return; + } +#endif + if (m_trayIcon && m_trayIcon->isVisible()) { // Hide the window instead of closing hide(); @@ -200,7 +277,7 @@ void Window::closeEvent(QCloseEvent *event) // Show a message to inform the user if (m_trayIcon->supportsMessages()) { m_trayIcon->showMessage( - tr("Logos Core POC"), + tr("Logos App"), tr("The application will continue to run in the system tray. " "Click the tray icon to restore the window."), QSystemTrayIcon::Information, diff --git a/app/window.h b/app/window.h index 2295eab..8057006 100644 --- a/app/window.h +++ b/app/window.h @@ -8,6 +8,9 @@ class LogosAPI; class QMenu; class QAction; class QCloseEvent; +class QResizeEvent; +class QShowEvent; +class QWidget; class Window : public QMainWindow { @@ -19,7 +22,10 @@ public: ~Window(); protected: + void changeEvent(QEvent *event) override; void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; private slots: void showHideWindow(); @@ -30,12 +36,16 @@ private: void setupUi(); void createTrayIcon(); void setIcon(); - +#ifdef Q_OS_MAC + void setupMacOSDockReopen(); +#endif + LogosAPI* m_logosAPI; QSystemTrayIcon* m_trayIcon; QMenu* m_trayIconMenu; QAction* m_showHideAction; QAction* m_quitAction; + QWidget* m_trafficLightsTitleBar = nullptr; }; -#endif // WINDOW_H \ No newline at end of file +#endif // WINDOW_H diff --git a/flake.nix b/flake.nix index 579a237..6336c09 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Logos App POC - Qt application with UI plugins"; + description = "Logos App - Qt application with UI plugins"; inputs = { # Follow the same nixpkgs as logos-liblogos to ensure compatibility @@ -168,7 +168,7 @@ export LOGOS_PACKAGE_MANAGER_SRC="${logosPackageManagerSrc}" export LOGOS_CAPABILITY_MODULE_SRC="${logosCapabilityModuleSrc}" - echo "Logos App POC development environment" + echo "Logos App development environment" echo "" echo "Nix packages (host builds):" echo " LOGOS_CPP_SDK_ROOT: $LOGOS_CPP_SDK_ROOT" diff --git a/logos_dapps/main_ui/src/MainContainer.cpp b/logos_dapps/main_ui/src/MainContainer.cpp index 3c3f313..1df3a91 100644 --- a/logos_dapps/main_ui/src/MainContainer.cpp +++ b/logos_dapps/main_ui/src/MainContainer.cpp @@ -100,7 +100,7 @@ void MainContainer::setupUi() // Create main horizontal layout m_mainLayout = new QHBoxLayout(this); m_mainLayout->setSpacing(0); - m_mainLayout->setContentsMargins(4, 2, 4, 2); + m_mainLayout->setContentsMargins(4, 0, 4, 2); // When QML_UI is set, add it to each QML engine's import path so nested // components (e.g. SidebarIconButton) load from disk — no rebuild for UI changes. QString qmlUiPath = QProcessEnvironment::systemEnvironment().value("QML_UI", ""); @@ -119,8 +119,8 @@ void MainContainer::setupUi() qDebug() << "Sidebar engine import paths:" << m_sidebarWidget->engine()->importPathList(); m_sidebarWidget->rootContext()->setContextProperty("backend", m_backend); m_sidebarWidget->setSource(resolveQmlUrl("qml/panels/SidebarPanel.qml")); - m_sidebarWidget->setMinimumWidth(80); - m_sidebarWidget->setMaximumWidth(80); + m_sidebarWidget->setMinimumWidth(60); + m_sidebarWidget->setMaximumWidth(60); // set clear color to sidebar so that rounded corners don't show white m_sidebarWidget->setClearColor(bgColor); @@ -128,7 +128,7 @@ void MainContainer::setupUi() QWidget* contentArea = new QWidget(this); QVBoxLayout* contentLayout = new QVBoxLayout(contentArea); contentLayout->setSpacing(0); - contentLayout->setContentsMargins(4, 4, 4, 4); + contentLayout->setContentsMargins(4, 9, 4, 4); // Create content stack m_contentStack = new QStackedWidget(contentArea); m_contentStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); diff --git a/logos_dapps/main_ui/src/mdiview.cpp b/logos_dapps/main_ui/src/mdiview.cpp index 8d7bcc3..6cbe146 100644 --- a/logos_dapps/main_ui/src/mdiview.cpp +++ b/logos_dapps/main_ui/src/mdiview.cpp @@ -14,7 +14,6 @@ MdiView::MdiView(QWidget *parent) : QWidget(parent) , windowCounter(0) , m_mdiAddBtn(nullptr) - , m_emptyPlaceholder(nullptr) { setupUi(); addMdiWindow(); @@ -47,9 +46,6 @@ void MdiView::setupUi() setLayout(mainLayout); mdiArea->setViewMode(QMdiArea::TabbedView); - m_emptyPlaceholder = new MdiChild(mdiArea); - m_emptyPlaceholder->setVisible(false); - m_emptyPlaceholder->setGeometry(mdiArea->rect()); mdiArea->installEventFilter(this); // Ensure tab bar styling applies after the tab bar is created @@ -90,8 +86,7 @@ void MdiView::updateTabCloseButtons() if (mdiArea->viewMode() == QMdiArea::TabbedView) { QTabBar* tabBar = mdiArea->findChild(); if (tabBar) { - tabBar->setTabsClosable(true); - + tabBar->setTabsClosable(false); disconnect(tabBar, &QTabBar::tabCloseRequested, nullptr, nullptr); connect(tabBar, &QTabBar::tabCloseRequested, [this](int index) { @@ -112,8 +107,6 @@ void MdiView::updateTabCloseButtons() }); } } - - updateEmptyPlaceholder(); } void MdiView::insetTabBarGeometry(QTabBar *tabBar, int insetPx) @@ -151,8 +144,7 @@ void MdiView::customizeTabBarStyle(QTabBar* tabBar) tabBar->setElideMode(Qt::ElideRight); tabBar->setUsesScrollButtons(false); tabBar->setExpanding(false); - tabBar->setFixedHeight(44); - tabBar->setIconSize(QSize(20, 20)); + tabBar->setIconSize(QSize(15, 15)); QScroller::grabGesture(tabBar, QScroller::LeftMouseButtonGesture); QScroller::grabGesture(tabBar, QScroller::TouchGesture); QScrollerProperties props = QScroller::scroller(tabBar)->scrollerProperties(); @@ -172,16 +164,12 @@ void MdiView::customizeTabBarStyle(QTabBar* tabBar) background: #262626; color: #A4A4A4; - padding: 0px 24px 0px 32px; + padding: 0px 8px 0px 4px; margin-right: 10px; - margin-left: 0px; - - margin-top: 9px; - margin-bottom: 4px; - + border-top-left-radius: 10px; border-top-right-radius: 10px; - height: 35px; + height: 20px; min-width: 120px; } @@ -193,20 +181,43 @@ void MdiView::customizeTabBarStyle(QTabBar* tabBar) QTabBar::tab:hover { background: #262626; } - - QTabBar::close-button { - image: url(:/icons/close.png); - width: 14px; - height: 14px; - margin-left: 16px; - margin-top: 10px; - } - QTabBar::close-button:hover { - background: #262626; - border-radius: 7px; - } )")); + installTabBarCloseButtons(tabBar); +} + +void MdiView::installTabBarCloseButtons(QTabBar* tabBar) +{ + if (!tabBar) return; + const QTabBar::ButtonPosition closeSide = QTabBar::LeftSide; + for (int i = 0; i < tabBar->count(); ++i) { + QWidget* oldBtn = tabBar->tabButton(i, closeSide); + if (oldBtn) { + tabBar->setTabButton(i, closeSide, nullptr); + oldBtn->deleteLater(); + } + QToolButton* btn = new QToolButton(tabBar); + btn->setIcon(QIcon(QStringLiteral(":/icons/close.png"))); + btn->setIconSize(QSize(12, 12)); + btn->setFixedSize(12, 12); + btn->setCursor(Qt::PointingHandCursor); + btn->setStyleSheet(QStringLiteral(R"( + QToolButton { background: transparent; border: none; } + QToolButton:hover { background: rgba(255,255,255,0.1); border-radius: 6px; } + )")); + connect(btn, &QToolButton::clicked, this, [tabBar, btn, closeSide]() { + for (int j = 0; j < tabBar->count(); ++j) { + if (tabBar->tabButton(j, closeSide) == btn) { + tabBar->tabCloseRequested(j); + break; + } + } + }); + btn->setVisible(false); // show only on tab hover + btn->installEventFilter(this); // keep visible when hovering the button itself + tabBar->setTabButton(i, closeSide, btn); + } + tabBar->setMouseTracking(true); // needed for hover-to-show close buttons } void MdiView::ensureMdiAddButton(QTabBar* tabBar) @@ -219,20 +230,17 @@ void MdiView::ensureMdiAddButton(QTabBar* tabBar) if (!m_mdiAddBtn) { m_mdiAddBtn = new QToolButton(tabBar->parentWidget()); m_mdiAddBtn->setIcon(QIcon(":/icons/add-button.png")); - m_mdiAddBtn->setIconSize(QSize(24, 24)); + m_mdiAddBtn->setIconSize(QSize(15, 15)); m_mdiAddBtn->setAutoRaise(true); m_mdiAddBtn->setCursor(Qt::PointingHandCursor); - m_mdiAddBtn->setFixedSize(42, 35); + m_mdiAddBtn->setFixedSize(25, 19); m_mdiAddBtn->setStyleSheet(QStringLiteral(R"( QToolButton { background: #2A2A2A; color: #FFFFFF; - border-top-left-radius: 14px; - border-top-right-radius: 14px; - padding-top: 9px; - padding-bottom: 2px; - padding-left: 0px; - padding-right: 0px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + padding-top: 1px; } QToolButton:hover { background: #262626; @@ -283,34 +291,29 @@ void MdiView::repositionMdiAddButton() m_mdiAddBtn->raise(); } -void MdiView::updateEmptyPlaceholder() -{ - if (!m_emptyPlaceholder || !mdiArea) - return; - - const bool empty = mdiArea->subWindowList().isEmpty(); - m_emptyPlaceholder->setVisible(empty); - if (empty) { - int top = 0; - if (QTabBar* bar = mdiArea->findChild()) - top = bar->mapTo(mdiArea, QPoint(0, bar->height())).y(); - m_emptyPlaceholder->setGeometry(0, top, mdiArea->width(), mdiArea->height() - top); - m_emptyPlaceholder->raise(); - } -} - bool MdiView::eventFilter(QObject* watched, QEvent* event) { - if (m_emptyPlaceholder && mdiArea && watched == mdiArea) { - if (event->type() == QEvent::Resize || event->type() == QEvent::Show) { - updateEmptyPlaceholder(); - } - } - QTabBar* tabBar = mdiArea->findChild(); if (tabBar && watched == tabBar) { if (event->type() == QEvent::Resize || event->type() == QEvent::Show) { repositionMdiAddButton(); + } else if (event->type() == QEvent::MouseMove) { + const QPoint pos = tabBar->mapFromGlobal(QCursor::pos()); + for (int i = 0; i < tabBar->count(); ++i) { + QWidget* closeBtn = tabBar->tabButton(i, QTabBar::LeftSide); + if (closeBtn) { + const QRect tabRect = tabBar->tabRect(i); + const bool overTabOrButton = tabRect.contains(pos) + || closeBtn->geometry().contains(pos); + closeBtn->setVisible(overTabOrButton); + } + } + } else if (event->type() == QEvent::Leave) { + for (int i = 0; i < tabBar->count(); ++i) { + QWidget* closeBtn = tabBar->tabButton(i, QTabBar::LeftSide); + if (closeBtn) + closeBtn->setVisible(false); + } } else if (event->type() == QEvent::Wheel && tabBar->count() > 1) { auto *wheelEvent = static_cast(event); int delta = 0; @@ -327,6 +330,20 @@ bool MdiView::eventFilter(QObject* watched, QEvent* event) } } } + + // Close button hover: show when pointer enters the button, hide on leave + if (tabBar) { + for (int i = 0; i < tabBar->count(); ++i) { + if (tabBar->tabButton(i, QTabBar::LeftSide) == watched) { + auto* w = static_cast(watched); + if (event->type() == QEvent::Enter) + w->setVisible(true); + else if (event->type() == QEvent::Leave) + w->setVisible(false); + return false; + } + } + } return QWidget::eventFilter(watched, event); } diff --git a/logos_dapps/main_ui/src/mdiview.h b/logos_dapps/main_ui/src/mdiview.h index d37e42f..d07747c 100644 --- a/logos_dapps/main_ui/src/mdiview.h +++ b/logos_dapps/main_ui/src/mdiview.h @@ -45,9 +45,9 @@ private: void ensureMdiAddButton(QTabBar* tabBar); void repositionMdiAddButton(); - void updateEmptyPlaceholder(); void customizeTabBarStyle(QTabBar* tabBar); + void installTabBarCloseButtons(QTabBar* tabBar); void insetTabBarGeometry(QTabBar *tabBar, int insetPx); bool eventFilter(QObject* watched, QEvent* event) override; @@ -57,8 +57,7 @@ private: QToolBar *toolBar; QVBoxLayout *mainLayout; QToolButton* m_mdiAddBtn; - QWidget* m_emptyPlaceholder; - + // Map to keep track of plugin widgets and their MDI windows QMap m_pluginWindows; // Reverse map: subwindow -> widget diff --git a/logos_dapps/main_ui/src/qml/controls/SidebarCircleButton.qml b/logos_dapps/main_ui/src/qml/controls/SidebarCircleButton.qml index ab22d6d..3efe40c 100644 --- a/logos_dapps/main_ui/src/qml/controls/SidebarCircleButton.qml +++ b/logos_dapps/main_ui/src/qml/controls/SidebarCircleButton.qml @@ -5,8 +5,8 @@ import Logos.Theme AbstractButton { id: root - implicitHeight: 50 - implicitWidth: 50 + implicitHeight: 38 + implicitWidth: 38 // Dark gray pill background extending to left edge when active/highlighted background: Rectangle { diff --git a/logos_dapps/main_ui/src/qml/controls/SidebarCircleButtonContainer.qml b/logos_dapps/main_ui/src/qml/controls/SidebarCircleButtonContainer.qml index d9137a4..0158848 100644 --- a/logos_dapps/main_ui/src/qml/controls/SidebarCircleButtonContainer.qml +++ b/logos_dapps/main_ui/src/qml/controls/SidebarCircleButtonContainer.qml @@ -9,7 +9,7 @@ Control { property alias contentModel: repeater.model signal moduleClicked(string name, int index) - padding: Theme.spacing.medium + padding: Theme.spacing.small background: Rectangle { id: bg diff --git a/logos_dapps/main_ui/src/qml/panels/SidebarPanel.qml b/logos_dapps/main_ui/src/qml/panels/SidebarPanel.qml index ca714c0..364bc80 100644 --- a/logos_dapps/main_ui/src/qml/panels/SidebarPanel.qml +++ b/logos_dapps/main_ui/src/qml/panels/SidebarPanel.qml @@ -18,7 +18,8 @@ Control { signal updateLauncherIndex(int index) padding: 0 - topPadding: Theme.spacing.large + topPadding: Theme.spacing.large + _d.systemTitleBarPadding + topInset: _d.systemTitleBarPadding QtObject { id: _d @@ -39,6 +40,8 @@ Control { readonly property var unloadedApps: (root.launcherApps || []).filter(function(item) { return item && item.isLoaded === false }) + + readonly property int systemTitleBarPadding: Qt.platform.os === "osx" ? 30: 0 } background: Rectangle { @@ -51,8 +54,8 @@ Control { Image { // As per design - Layout.preferredWidth: 64 - Layout.preferredHeight: 34 + Layout.preferredWidth: 46 + Layout.preferredHeight: 25 Layout.alignment: Qt.AlignHCenter source: "qrc:/icons/basecamp.png" } diff --git a/nix/app.nix b/nix/app.nix index 7913b01..9126f2c 100644 --- a/nix/app.nix +++ b/nix/app.nix @@ -1,4 +1,4 @@ -# Builds the logos-app-poc standalone application +# Builds the logos-app standalone application { pkgs, common, src, logosLiblogos, logosSdk, logosPackageManager, logosCapabilityModule, logosDesignSystem, counterPlugin, counterQmlPlugin, mainUIPlugin, packageManagerUIPlugin, webviewAppPlugin }: let @@ -7,7 +7,7 @@ let webkitgtk = pkgs.webkitgtk_4_1 or pkgs.webkitgtk_4_0 or pkgs.webkitgtk; in pkgs.stdenv.mkDerivation rec { - pname = "logos-app-poc-app"; + pname = "logos-app"; version = common.version; inherit src; @@ -91,12 +91,9 @@ pkgs.stdenv.mkDerivation rec { # This is an aggregate runtime layout; avoid stripping to prevent hook errors dontStrip = true; - # Ensure proper Qt environment setup via wrapper - qtWrapperArgs = [ - "--prefix" "LD_LIBRARY_PATH" ":" qtLibPath - "--prefix" "QT_PLUGIN_PATH" ":" qtPluginPath - "--prefix" "QML2_IMPORT_PATH" ":" qmlImportPath - ]; + # Skip wrapQtApps: wrapper renames binary to .LogosApp-wrapped; macOS Dock uses executable filename + # We create a custom launcher that execs the binary (keeps process name "LogosApp") + dontWrapQtApps = true; # Additional environment variables for Qt and RPATH cleanup preFixup = '' @@ -136,7 +133,7 @@ pkgs.stdenv.mkDerivation rec { configurePhase = '' runHook preConfigure - echo "Configuring logos-app-poc-app..." + echo "Configuring logos-app..." echo "liblogos: ${logosLiblogos}" echo "cpp-sdk: ${logosSdk}" echo "package-manager: ${logosPackageManager}" @@ -177,7 +174,7 @@ pkgs.stdenv.mkDerivation rec { runHook preBuild cmake --build build - echo "logos-app-poc-app built successfully!" + echo "logos-app built successfully!" runHook postBuild ''; @@ -271,12 +268,20 @@ pkgs.stdenv.mkDerivation rec { # Note: webview_app QML and HTML files are now embedded in the plugin via qrc - # Create symlink for the expected binary name - ln -s $out/bin/LogosApp $out/bin/logos-app-poc-app + # Create launcher script (sets Qt env, execs binary - process name stays "LogosApp" for Dock) + cat > $out/bin/logos-app << 'EOF' +#!/bin/sh +EOF + echo "export QT_PLUGIN_PATH=\"${qtPluginPath}\"" >> $out/bin/logos-app + echo "export QML2_IMPORT_PATH=\"${qmlImportPath}\"" >> $out/bin/logos-app + echo "export DYLD_LIBRARY_PATH=\"${qtLibPath}:\$DYLD_LIBRARY_PATH\"" >> $out/bin/logos-app + echo "export LD_LIBRARY_PATH=\"${qtLibPath}:\$LD_LIBRARY_PATH\"" >> $out/bin/logos-app + echo 'exec "$(dirname "$0")/LogosApp" "$@"' >> $out/bin/logos-app + chmod +x $out/bin/logos-app # Create a README for reference cat > $out/README.txt < "$appDir/usr/share/applications/logos-app.desktop" <<'EOF' [Desktop Entry] Type=Application -Name=Logos App POC +Name=Logos App Exec=LogosApp Icon=logos-app Categories=Utility; @@ -114,7 +114,7 @@ EOF ''; meta = with pkgs.lib; { - description = "Logos App POC AppImage"; + description = "Logos App AppImage"; platforms = platforms.linux; mainProgram = "LogosApp"; }; diff --git a/nix/default.nix b/nix/default.nix index 532c010..2117cab 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -2,7 +2,7 @@ { pkgs, logosSdk, logosLiblogos }: { - pname = "logos-app-poc"; + pname = "logos-app"; version = "1.0.0"; # Common native build inputs @@ -40,7 +40,7 @@ # Metadata meta = with pkgs.lib; { - description = "Logos App POC - Qt application with UI plugins"; + description = "Logos App - Qt application with UI plugins"; platforms = platforms.unix; }; } diff --git a/run-dev.sh b/run-dev.sh index 15ed389..58b1f9d 100755 --- a/run-dev.sh +++ b/run-dev.sh @@ -30,22 +30,22 @@ fi # Print the paths being used echo "================================================" -echo "Starting Logos App POC in DEVELOPMENT mode" +echo "Starting Logos App in DEVELOPMENT mode" echo "================================================" echo "QML_UI path: $QML_UI" echo "" echo "QML files will be loaded from the filesystem." echo "The QML_UI path is also added to the engine import path, so" echo "nested components (e.g. SidebarIconButton) load from disk too." -echo "Edit QML files and restart the app to see changes (no rebuild)." echo "================================================" echo "" # Run the app from the nix result -if [ -f "./result/bin/LogosApp" ]; then +# Use logos-app launcher (sets Qt env, execs LogosApp binary - Dock shows "LogosApp") +if [ -f "./result/bin/logos-app" ]; then + ./result/bin/logos-app "$@" +elif [ -f "./result/bin/LogosApp" ]; then ./result/bin/LogosApp "$@" -elif [ -f "./result/bin/logos-app-poc-app" ]; then - ./result/bin/logos-app-poc-app "$@" else echo "Error: Application binary not found in ./result/bin/" echo "Please build the app first with: nix build"