2025-10-08 13:38:08 -04:00
|
|
|
#include "window.h"
|
|
|
|
|
#include <QApplication>
|
|
|
|
|
#include <QScreen>
|
|
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QLabel>
|
|
|
|
|
#include <QVBoxLayout>
|
|
|
|
|
#include <QPluginLoader>
|
|
|
|
|
#include <QDir>
|
2026-03-27 10:57:53 -03:00
|
|
|
#include <QFile>
|
2025-11-25 11:36:34 -05:00
|
|
|
#include <QSystemTrayIcon>
|
|
|
|
|
#include <QMenu>
|
|
|
|
|
#include <QAction>
|
|
|
|
|
#include <QCloseEvent>
|
|
|
|
|
#include <QIcon>
|
|
|
|
|
#include <QPixmap>
|
2025-12-09 10:52:52 -05:00
|
|
|
#include <IComponent.h>
|
2026-02-26 11:05:34 -03:00
|
|
|
#include <QStandardPaths>
|
2026-02-05 23:11:12 +05:30
|
|
|
#include <QTimer>
|
2026-03-18 14:17:05 +01:00
|
|
|
#include "LogosBasecampPaths.h"
|
2026-02-05 23:11:12 +05:30
|
|
|
#ifdef Q_OS_MAC
|
|
|
|
|
#include "trafficLightsTitleBar.h"
|
|
|
|
|
#include "macWindowStyle.h"
|
|
|
|
|
#endif
|
2025-10-08 13:38:08 -04:00
|
|
|
|
|
|
|
|
Window::Window(QWidget *parent)
|
|
|
|
|
: QMainWindow(parent)
|
|
|
|
|
, m_logosAPI(nullptr)
|
2025-11-25 11:36:34 -05:00
|
|
|
, m_trayIcon(nullptr)
|
|
|
|
|
, m_trayIconMenu(nullptr)
|
|
|
|
|
, m_showHideAction(nullptr)
|
|
|
|
|
, m_quitAction(nullptr)
|
2025-10-08 13:38:08 -04:00
|
|
|
{
|
|
|
|
|
setupUi();
|
2025-11-25 11:36:34 -05:00
|
|
|
createTrayIcon();
|
2025-10-08 13:38:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Window::Window(LogosAPI* logosAPI, QWidget *parent)
|
|
|
|
|
: QMainWindow(parent)
|
|
|
|
|
, m_logosAPI(logosAPI)
|
2025-11-25 11:36:34 -05:00
|
|
|
, m_trayIcon(nullptr)
|
|
|
|
|
, m_trayIconMenu(nullptr)
|
|
|
|
|
, m_showHideAction(nullptr)
|
|
|
|
|
, m_quitAction(nullptr)
|
2025-10-08 13:38:08 -04:00
|
|
|
{
|
|
|
|
|
setupUi();
|
2025-11-25 11:36:34 -05:00
|
|
|
createTrayIcon();
|
2025-10-08 13:38:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Window::~Window()
|
|
|
|
|
{
|
2025-11-25 11:36:34 -05:00
|
|
|
if (m_trayIcon) {
|
|
|
|
|
delete m_trayIcon;
|
|
|
|
|
}
|
2025-10-08 13:38:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::setupUi()
|
|
|
|
|
{
|
|
|
|
|
// Determine the appropriate plugin extension based on the platform
|
|
|
|
|
QString pluginExtension;
|
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
|
pluginExtension = ".dll";
|
|
|
|
|
#elif defined(Q_OS_MAC)
|
|
|
|
|
pluginExtension = ".dylib";
|
|
|
|
|
#else // Linux and other Unix-like systems
|
|
|
|
|
pluginExtension = ".so";
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
QString embeddedPluginsDir = LogosBasecampPaths::embeddedPluginsDirectory() + "/";
|
2026-03-18 14:17:05 +01:00
|
|
|
QString userPluginsDir = LogosBasecampPaths::pluginsDirectory() + "/";
|
2026-02-26 11:05:34 -03:00
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
// Search embedded (pre-installed at build time) first, then user-writable directory.
|
2026-02-26 11:05:34 -03:00
|
|
|
auto resolvePlugin = [&](const QString& subdir, const QString& name) -> QString {
|
2026-03-27 10:57:53 -03:00
|
|
|
QString embeddedPath = embeddedPluginsDir + subdir + "/" + name + pluginExtension;
|
|
|
|
|
if (QFile::exists(embeddedPath))
|
|
|
|
|
return embeddedPath;
|
2026-02-26 11:05:34 -03:00
|
|
|
return userPluginsDir + subdir + "/" + name + pluginExtension;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-23 11:14:48 -05:00
|
|
|
// Load the main_ui plugin with the appropriate extension (now in subdirectory)
|
2026-02-26 11:05:34 -03:00
|
|
|
QString mainUiPluginPath = resolvePlugin("main_ui", "main_ui");
|
2025-12-09 10:52:52 -05:00
|
|
|
QPluginLoader loader(mainUiPluginPath);
|
2025-10-08 13:38:08 -04:00
|
|
|
|
|
|
|
|
QWidget* mainContent = nullptr;
|
2025-12-09 10:52:52 -05:00
|
|
|
QObject* mainUiPlugin = nullptr;
|
2025-10-08 13:38:08 -04:00
|
|
|
|
|
|
|
|
if (loader.load()) {
|
2025-12-09 10:52:52 -05:00
|
|
|
mainUiPlugin = loader.instance();
|
|
|
|
|
if (mainUiPlugin) {
|
2025-10-08 13:38:08 -04:00
|
|
|
// Try to create the main window using the plugin's createWidget method
|
2025-12-09 10:52:52 -05:00
|
|
|
QMetaObject::invokeMethod(mainUiPlugin, "createWidget",
|
2025-10-08 13:38:08 -04:00
|
|
|
Qt::DirectConnection,
|
|
|
|
|
Q_RETURN_ARG(QWidget*, mainContent),
|
|
|
|
|
Q_ARG(LogosAPI*, m_logosAPI));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mainContent) {
|
|
|
|
|
setCentralWidget(mainContent);
|
|
|
|
|
} else {
|
|
|
|
|
qWarning() << "================================================";
|
2025-12-09 10:52:52 -05:00
|
|
|
qWarning() << "Failed to load main UI plugin from:" << mainUiPluginPath;
|
2025-10-08 13:38:08 -04:00
|
|
|
qWarning() << "Error:" << loader.errorString();
|
|
|
|
|
qWarning() << "================================================";
|
|
|
|
|
// Fallback: show a message when plugin is not found
|
|
|
|
|
QWidget* fallbackWidget = new QWidget(this);
|
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(fallbackWidget);
|
|
|
|
|
|
|
|
|
|
QLabel* messageLabel = new QLabel("No main UI module found", fallbackWidget);
|
|
|
|
|
QFont font = messageLabel->font();
|
|
|
|
|
font.setPointSize(14);
|
|
|
|
|
messageLabel->setFont(font);
|
|
|
|
|
messageLabel->setAlignment(Qt::AlignCenter);
|
|
|
|
|
|
|
|
|
|
layout->addWidget(messageLabel);
|
|
|
|
|
setCentralWidget(fallbackWidget);
|
2025-12-09 10:52:52 -05:00
|
|
|
qWarning() << "Failed to load main UI plugin from:" << mainUiPluginPath;
|
2025-10-08 13:38:08 -04:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 21:52:58 +01:00
|
|
|
// Set window title and size
|
2026-05-25 13:03:20 -04:00
|
|
|
setWindowTitle("Logos Basecamp");
|
2025-10-08 13:38:08 -04:00
|
|
|
resize(1024, 768);
|
2026-02-05 23:11:12 +05:30
|
|
|
|
|
|
|
|
#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
|
2025-11-25 11:36:34 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 23:11:12 +05:30
|
|
|
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
|
|
|
|
|
|
2025-11-25 11:36:34 -05:00
|
|
|
void Window::createTrayIcon()
|
|
|
|
|
{
|
|
|
|
|
if (!QSystemTrayIcon::isSystemTrayAvailable()) {
|
|
|
|
|
qWarning() << "System tray is not available on this system";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create tray icon
|
|
|
|
|
m_trayIcon = new QSystemTrayIcon(this);
|
|
|
|
|
setIcon();
|
2026-05-25 13:03:20 -04:00
|
|
|
m_trayIcon->setToolTip("Logos Basecamp");
|
2025-11-25 11:36:34 -05:00
|
|
|
|
|
|
|
|
// Create context menu
|
|
|
|
|
m_trayIconMenu = new QMenu(this);
|
|
|
|
|
|
|
|
|
|
m_showHideAction = m_trayIconMenu->addAction(tr("Show/Hide"));
|
|
|
|
|
m_showHideAction->setCheckable(false);
|
|
|
|
|
connect(m_showHideAction, &QAction::triggered, this, &Window::showHideWindow);
|
|
|
|
|
|
|
|
|
|
m_trayIconMenu->addSeparator();
|
|
|
|
|
|
|
|
|
|
m_quitAction = m_trayIconMenu->addAction(tr("Quit"));
|
|
|
|
|
connect(m_quitAction, &QAction::triggered, this, &Window::quitApplication);
|
|
|
|
|
|
|
|
|
|
m_trayIcon->setContextMenu(m_trayIconMenu);
|
|
|
|
|
|
|
|
|
|
// Connect icon activation signal
|
|
|
|
|
connect(m_trayIcon, &QSystemTrayIcon::activated, this, &Window::iconActivated);
|
|
|
|
|
|
|
|
|
|
// Show the tray icon
|
|
|
|
|
m_trayIcon->show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::setIcon()
|
|
|
|
|
{
|
|
|
|
|
if (!m_trayIcon) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QIcon icon(":/icons/logos.png");
|
|
|
|
|
if (icon.isNull()) {
|
|
|
|
|
qWarning() << "Failed to load tray icon from resource";
|
|
|
|
|
// Create a simple fallback icon
|
|
|
|
|
icon = QIcon::fromTheme("application-x-executable");
|
|
|
|
|
if (icon.isNull()) {
|
|
|
|
|
// Last resort: create a minimal icon
|
|
|
|
|
QPixmap pixmap(16, 16);
|
|
|
|
|
pixmap.fill(Qt::blue);
|
|
|
|
|
icon = QIcon(pixmap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m_trayIcon->setIcon(icon);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::closeEvent(QCloseEvent *event)
|
|
|
|
|
{
|
2026-02-05 23:11:12 +05:30
|
|
|
#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
|
|
|
|
|
|
2025-11-25 11:36:34 -05:00
|
|
|
if (m_trayIcon && m_trayIcon->isVisible()) {
|
|
|
|
|
// Hide the window instead of closing
|
|
|
|
|
hide();
|
|
|
|
|
event->ignore();
|
|
|
|
|
|
|
|
|
|
// Show a message to inform the user
|
|
|
|
|
if (m_trayIcon->supportsMessages()) {
|
|
|
|
|
m_trayIcon->showMessage(
|
2026-05-25 13:03:20 -04:00
|
|
|
tr("Logos Basecamp"),
|
2025-11-25 11:36:34 -05:00
|
|
|
tr("The application will continue to run in the system tray. "
|
|
|
|
|
"Click the tray icon to restore the window."),
|
|
|
|
|
QSystemTrayIcon::Information,
|
|
|
|
|
2000
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// If system tray is not available, quit normally
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::showHideWindow()
|
|
|
|
|
{
|
|
|
|
|
if (isVisible()) {
|
|
|
|
|
hide();
|
|
|
|
|
} else {
|
|
|
|
|
show();
|
|
|
|
|
raise();
|
|
|
|
|
activateWindow();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::iconActivated(QSystemTrayIcon::ActivationReason reason)
|
|
|
|
|
{
|
|
|
|
|
switch (reason) {
|
|
|
|
|
case QSystemTrayIcon::Trigger:
|
|
|
|
|
// Single click - toggle window visibility
|
|
|
|
|
showHideWindow();
|
|
|
|
|
break;
|
|
|
|
|
case QSystemTrayIcon::DoubleClick:
|
|
|
|
|
// Double click - also toggle (some platforms use double click)
|
|
|
|
|
showHideWindow();
|
|
|
|
|
break;
|
|
|
|
|
case QSystemTrayIcon::MiddleClick:
|
|
|
|
|
// Middle click - toggle as well
|
|
|
|
|
showHideWindow();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Window::quitApplication()
|
|
|
|
|
{
|
|
|
|
|
// Hide tray icon first
|
|
|
|
|
if (m_trayIcon) {
|
|
|
|
|
m_trayIcon->hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Quit the application
|
|
|
|
|
QApplication::quit();
|
2025-10-08 13:38:08 -04:00
|
|
|
}
|