feat: Making macos traffic buttons to be overlayed on top of app and alligning app for this to look okay

This commit is contained in:
Khushboo Mehta
2026-02-16 15:43:40 +01:00
parent d2d7af1975
commit 2f199aa04d
25 changed files with 583 additions and 113 deletions
+3 -3
View File
@@ -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.
+14 -2
View File
@@ -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)
+1 -1
View File
@@ -1,4 +1,4 @@
# Logos Core POC
# Logos App
## Building the Application
Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>LogosApp</string>
<string>Logos App</string>
<key>CFBundleExecutable</key>
<string>LogosApp</string>
<key>CFBundleIconFile</key>
@@ -15,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>LogosApp</string>
<string>Logos App</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
+8
View File
@@ -0,0 +1,8 @@
#ifndef MACWINDOWSTYLE_H
#define MACWINDOWSTYLE_H
class QMainWindow;
void applyMacWindowRoundedCorners(QMainWindow* w, bool rounded = true);
#endif // MACWINDOWSTYLE_H
+25
View File
@@ -0,0 +1,25 @@
#include <QtCore/qglobal.h>
#ifdef Q_OS_MAC
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
#endif
#include "macWindowStyle.h"
#include <QMainWindow>
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
}
+263
View File
@@ -0,0 +1,263 @@
#include "trafficLightsTitleBar.h"
#include <QPushButton>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QWindow>
#include <QIcon>
#include <QApplication>
#include <QMainWindow>
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<QMainWindow*>(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<QMainWindow*>(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();
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef TRAFFICLIGHTSTITLEBAR_H
#define TRAFFICLIGHTSTITLEBAR_H
#include <QWidget>
#include <QPoint>
#include <QEvent>
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
+3
View File
@@ -2,5 +2,8 @@
<RCC>
<qresource prefix="/">
<file>icons/logos.png</file>
<file>icons/trafficlights/close.png</file>
<file>icons/trafficlights/minimise.png</file>
<file>icons/trafficlights/maximize.png</file>
</qresource>
</RCC>
+83 -6
View File
@@ -13,6 +13,11 @@
#include <QIcon>
#include <QPixmap>
#include <IComponent.h>
#include <QTimer>
#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,
+12 -2
View File
@@ -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
#endif // WINDOW_H
+2 -2
View File
@@ -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"
+4 -4
View File
@@ -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);
+76 -59
View File
@@ -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<QTabBar*>();
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<QTabBar*>())
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<QTabBar*>();
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<QWheelEvent*>(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<QWidget*>(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);
}
+2 -3
View File
@@ -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<QWidget*, QMdiSubWindow*> m_pluginWindows;
// Reverse map: subwindow -> widget
@@ -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 {
@@ -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
@@ -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"
}
+18 -13
View File
@@ -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 <<EOF
Logos App POC - Build Information
Logos App - Build Information
==================================
liblogos: ${logosLiblogos}
cpp-sdk: ${logosSdk}
+3 -3
View File
@@ -35,7 +35,7 @@ let
runtimeLibsStr = pkgs.lib.concatStringsSep " " (map toString runtimeLibs);
in
pkgs.stdenv.mkDerivation rec {
pname = "logos-app-poc-appimage";
pname = "logos-app-appimage";
inherit version;
dontUnpack = true;
@@ -77,7 +77,7 @@ pkgs.stdenv.mkDerivation rec {
cat > "$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";
};
+2 -2
View File
@@ -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;
};
}
+5 -5
View File
@@ -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"