From 6b9fdd9299678982f5c872ad07e43de0d438bafa Mon Sep 17 00:00:00 2001 From: uncor3 Date: Mon, 29 Sep 2025 04:29:13 -0700 Subject: [PATCH] refactor ui - Implemented CustomTab class with notification support and custom painting. - Created CustomTabWidget to manage multiple tabs with a stacked widget layout. - Integrated glider animation for smooth tab transitions. - Updated DeviceInfoWidget to improve layout and visual appearance with shadows. - Refactored DeviceMenuWidget to use QStackedWidget instead of QTabWidget for better flexibility. - Enhanced main window setup with custom tab widget and improved device management UI. - Added macOS specific window styling for a more native look. - Improved ToolboxWidget layout and styling for better user experience. --- CMakeLists.txt | 3 + icons/MdiLightningBolt.png | Bin 0 -> 9548 bytes icons/MingcuteSettings7Line.png | Bin 0 -> 20162 bytes resources.qrc | 2 + src/appcontext.cpp | 5 + src/appcontext.h | 1 + src/batterywidget.cpp | 15 +- src/customtabwidget.cpp | 329 ++++++++++++++++++++++++++++++++ src/customtabwidget.h | 72 +++++++ src/deviceinfowidget.cpp | 122 +++++++++--- src/deviceinfowidget.h | 5 +- src/devicemenuwidget.cpp | 57 +++--- src/devicemenuwidget.h | 6 +- src/devicesidebarwidget.cpp | 2 +- src/iDescriptor-ui.h | 8 +- src/mainwindow.cpp | 125 +++++++++--- src/mainwindow.h | 22 +-- src/mainwindow.ui | 138 +------------- src/platform/macos.mm | 42 ++++ src/toolboxwidget.cpp | 16 +- 20 files changed, 725 insertions(+), 245 deletions(-) create mode 100644 icons/MdiLightningBolt.png create mode 100644 icons/MingcuteSettings7Line.png create mode 100644 src/customtabwidget.cpp create mode 100644 src/customtabwidget.h create mode 100644 src/platform/macos.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index c072041..7dbbe77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,9 @@ file(GLOB PROJECT_SOURCES src/*.cpp src/core/helpers/*.cpp src/core/services/*.cpp + src/platform/*.cpp + src/platform/*.mm + src/platform/*.m src/*.h src/*.ui resources.qrc diff --git a/icons/MdiLightningBolt.png b/icons/MdiLightningBolt.png new file mode 100644 index 0000000000000000000000000000000000000000..b4daaf25821253374e2b2154b5c309387cf89d27 GIT binary patch literal 9548 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 literal 0 HcmV?d00001 diff --git a/icons/MingcuteSettings7Line.png b/icons/MingcuteSettings7Line.png new file mode 100644 index 0000000000000000000000000000000000000000..6f195806f7748541a4c8a15fa96909e113f964dc GIT binary patch literal 20162 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 icons/ArrowMoveDownRight.svg icons/video-x-generic.png + icons/MdiLightningBolt.png + icons/MingcuteSettings7Line.png qml/MapView.qml resources/dump.js resources/iphone.png diff --git a/src/appcontext.cpp b/src/appcontext.cpp index ac2a509..9e78da4 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -153,6 +153,11 @@ void AppContext::instanceRemoveDevice(QString _udid) // return true; } +int AppContext::getConnectedDeviceCount() const +{ + return m_devices.size() + m_recoveryDevices.size(); +} + void AppContext::removeDevice(QString _udid) { diff --git a/src/appcontext.h b/src/appcontext.h index f330187..23f10b4 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -26,6 +26,7 @@ public: QList getAllRecoveryDevices(); ~AppContext(); void instanceRemoveDevice(QString _udid); + int getConnectedDeviceCount() const; private: QMap m_devices; diff --git a/src/batterywidget.cpp b/src/batterywidget.cpp index 533334d..5617deb 100644 --- a/src/batterywidget.cpp +++ b/src/batterywidget.cpp @@ -10,8 +10,8 @@ BatteryWidget::BatteryWidget(float value, bool isCharging, QWidget *parent) : QWidget(parent), m_value(value), m_isCharging(isCharging) { - setMinimumSize(50, 40); - setMaximumSize(60, 40); + setMinimumSize(30, 30); + setMaximumSize(40, 40); } void BatteryWidget::resizeEvent(QResizeEvent *) @@ -111,7 +111,7 @@ void BatteryWidget::paintEvent(QPaintEvent *) pen.setColor(Qt::white); painter.setPen(pen); QFont textFont = QFont(); - textFont.setPixelSize(widgetFrame.height() / 2); + textFont.setPixelSize(widgetFrame.height() / 1.65); painter.setFont(textFont); QFontMetrics fm(textFont); QString percentageLevelString = QString("%1%").arg(m_value); @@ -121,13 +121,4 @@ void BatteryWidget::paintEvent(QPaintEvent *) QPointF textPosition = QPointF(widgetFrame.center().x() - textWidth / 2, widgetFrame.center().y() + textHeight / 3); painter.drawText(textPosition, percentageLevelString); - - float chargerSize = widgetFrame.height() / 2; - - // if (isCharging) { - // QPixmap pixmap(":/img/charge.png"); - // painter.drawPixmap(widgetFrame.center().x() - chargerSize * 1.5, - // widgetFrame.top() + chargerSize / 2, chargerSize, - // chargerSize, pixmap); - // } } \ No newline at end of file diff --git a/src/customtabwidget.cpp b/src/customtabwidget.cpp new file mode 100644 index 0000000..6250e93 --- /dev/null +++ b/src/customtabwidget.cpp @@ -0,0 +1,329 @@ +#include "customtabwidget.h" +#include +#include +#include +#include +#include +#include + +// CustomTab implementation +CustomTab::CustomTab(const QString &text, QWidget *parent) + : QPushButton(text, parent), m_notificationLabel(nullptr), + m_notificationCount(0) +{ + setCheckable(true); + setFixedHeight(54); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + // Set up notification label + m_notificationLabel = new QLabel(this); + m_notificationLabel->setAlignment(Qt::AlignCenter); + m_notificationLabel->hide(); + m_notificationLabel->setStyleSheet("QLabel {" + " background-color: #e6eef9;" + " border-radius: 16px;" + " color: #333;" + " font-weight: 500;" + " min-width: 32px;" + " min-height: 32px;" + " max-width: 32px;" + " max-height: 32px;" + "}"); + + updateNotificationDisplay(); +} + +void CustomTab::setNotificationCount(int count) +{ + m_notificationCount = count; + updateNotificationDisplay(); +} + +void CustomTab::setIcon(const QIcon &icon) +{ + QPushButton::setIcon(icon); + setIconSize(QSize(20, 20)); +} + +void CustomTab::updateNotificationDisplay() +{ + if (m_notificationCount > 0) { + m_notificationLabel->setText(QString::number(m_notificationCount)); + m_notificationLabel->show(); + + // Position notification label to the right of the text + QFontMetrics fm(font()); + int textWidth = fm.horizontalAdvance(text()); + int iconWidth = iconSize().width(); + int totalContentWidth = (iconWidth > 0 ? iconWidth + 8 : 0) + textWidth; + + int x = (width() - totalContentWidth) / 2 + totalContentWidth + 12; + int y = (height() - 32) / 2; + + m_notificationLabel->setGeometry(x, y, 32, 32); + } else { + m_notificationLabel->hide(); + } +} + +void CustomTab::paintEvent(QPaintEvent *event) +{ + QPushButton::paintEvent(event); + updateNotificationDisplay(); + + // Update notification label style based on checked state + if (isChecked()) { + m_notificationLabel->setStyleSheet("QLabel {" + " background-color: #185ee0;" + " border-radius: 16px;" + " color: white;" + " font-weight: 500;" + " min-width: 32px;" + " min-height: 32px;" + " max-width: 32px;" + " max-height: 32px;" + "}"); + } else { + m_notificationLabel->setStyleSheet("QLabel {" + " background-color: #e6eef9;" + " border-radius: 16px;" + " color: #333;" + " font-weight: 500;" + " min-width: 32px;" + " min-height: 32px;" + " max-width: 32px;" + " max-height: 32px;" + "}"); + } +} + +// CustomTabWidget implementation +CustomTabWidget::CustomTabWidget(QWidget *parent) + : QWidget(parent), m_currentIndex(0) +{ + m_mainLayout = new QVBoxLayout(this); + m_mainLayout->setContentsMargins(0, 0, 0, 0); + m_mainLayout->setSpacing(0); + + // Create tab bar container + m_tabBar = new QWidget(); + m_tabBar->setFixedHeight(70); // 54px height + 16px padding + m_tabLayout = new QHBoxLayout(m_tabBar); + // m_tabLayout->setContentsMargins(12, 8, 12, 8); + m_tabLayout->setSpacing(0); + + // Style the tab bar + m_tabBar->setStyleSheet("QWidget {" + // " background-color: white;" + " border-radius: 35px;" + "}"); + + // Add drop shadow effect + QGraphicsDropShadowEffect *shadow = new QGraphicsDropShadowEffect(); + shadow->setBlurRadius(20); + shadow->setColor(QColor(24, 94, 224, 38)); // rgba(24, 94, 224, 0.15) + shadow->setOffset(0, 6); + m_tabBar->setGraphicsEffect(shadow); + + m_buttonGroup = new QButtonGroup(this); + m_buttonGroup->setExclusive(true); + + // Create stacked widget for content + m_stackedWidget = new QStackedWidget(); + + // Add widgets to layout + m_mainLayout->addWidget(m_tabBar); + m_mainLayout->addWidget(m_stackedWidget, 1); + + setupGlider(); +} + +void CustomTabWidget::setupGlider() +{ + m_glider = new QWidget(m_tabBar); + m_glider->setStyleSheet("QWidget {" + " background-color: #185ee0;" + " border-radius: 1px;" + "}"); + // Set initial size - will be updated in animateGlider + m_glider->setFixedSize(100, 2); // 2px height for bottom border effect + m_glider->lower(); // Make sure glider is behind tabs + + m_gliderAnimation = new QPropertyAnimation(m_glider, "pos"); + m_gliderAnimation->setDuration(250); + m_gliderAnimation->setEasingCurve(QEasingCurve::OutCubic); +} + +int CustomTabWidget::addTab(QWidget *widget, const QString &label) +{ + return addTab(widget, QIcon(), label); +} + +int CustomTabWidget::addTab(QWidget *widget, const QIcon &icon, + const QString &label) +{ + CustomTab *tab = new CustomTab(label, m_tabBar); + if (!icon.isNull()) { + tab->setIcon(icon); + } + connect(tab, &CustomTab::clicked, this, &CustomTabWidget::onTabClicked); + int index = m_tabs.count(); + m_tabs.append(tab); + m_widgets.append(widget); + + m_tabLayout->addWidget(tab); + m_stackedWidget->addWidget(widget); + m_buttonGroup->addButton(tab, index); + + // Set first tab as checked by default + if (index == 0) { + tab->setChecked(true); + } + + return index; +} + +void CustomTabWidget::setCurrentIndex(int index) +{ + if (index < 0 || index >= m_tabs.count() || index == m_currentIndex) { + return; + } + + m_currentIndex = index; + m_tabs[index]->setChecked(true); + m_stackedWidget->setCurrentIndex(index); + updateTabStyles(); + animateGlider(index); + + emit currentChanged(index); +} + +void CustomTabWidget::finalizeStyles() +{ + updateTabStyles(); + // Position glider for first tab + QTimer::singleShot(0, [this]() { animateGlider(0); }); +} + +int CustomTabWidget::currentIndex() const { return m_currentIndex; } + +QWidget *CustomTabWidget::widget(int index) const +{ + if (index < 0 || index >= m_widgets.count()) { + return nullptr; + } + return m_widgets[index]; +} + +void CustomTabWidget::setTabNotification(int index, int count) +{ + if (index >= 0 && index < m_tabs.count()) { + m_tabs[index]->setNotificationCount(count); + } +} + +void CustomTabWidget::onTabClicked() +{ + CustomTab *clickedTab = qobject_cast(sender()); + if (!clickedTab) + return; + + int index = m_tabs.indexOf(clickedTab); + if (index != -1) { + setCurrentIndex(index); + } +} + +void CustomTabWidget::animateGlider(int index) +{ + if (index < 0 || index >= m_tabs.count()) + return; + + CustomTab *targetTab = m_tabs[index]; + if (!targetTab) + return; + + // Get the actual position and size of the target tab + QPoint targetTabPos = targetTab->pos(); + QSize targetTabSize = targetTab->size(); + + // Set glider width to match tab width and height to 2px for bottom border + m_glider->setFixedSize(targetTabSize.width(), 2); + + // Position glider at the bottom of the target tab + int targetX = targetTabPos.x(); + int targetY = + targetTabPos.y() + targetTabSize.height() - 2; // Position at bottom + + m_gliderAnimation->stop(); + m_gliderAnimation->setStartValue(m_glider->pos()); + m_gliderAnimation->setEndValue(QPoint(targetX, targetY)); + m_gliderAnimation->start(); +} + +void CustomTabWidget::updateTabStyles() +{ + for (int i = 0; i < m_tabs.count(); ++i) { + CustomTab *tab = m_tabs[i]; + if (tab->isChecked()) { + tab->setStyleSheet("CustomTab {" + " color: #185ee0;" + " font-weight: 500;" + " font-size: 20px;" + " border: none;" + " border-radius: 27px;" + " background-color: transparent;" + "}" + "CustomTab:hover {" + " background-color: transparent;" + "}"); + } else { + tab->setStyleSheet("CustomTab {" + " color: #666;" + " font-weight: 500;" + " font-size: 20px;" + " border: none;" + " border-radius: 27px;" + " background-color: transparent;" + "}" + "CustomTab:hover {" + " color: #185ee0;" + " background-color: transparent;" + "}"); + } + } +} + +void CustomTabWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + + // Update glider position when widget is resized + if (m_currentIndex >= 0 && m_currentIndex < m_tabs.count()) { + // Use a timer to ensure layout has been updated + QTimer::singleShot(0, [this]() { animateGlider(m_currentIndex); }); + } +} + +// #ifdef Q_OS_MAC +// void CustomTabWidget::ensureTitlebarIntegration() +// { +// // Ensure the tab bar maintains the correct height and margins for +// titlebar integration m_tabBar->setFixedHeight(98); // 70px + 28px +// titlebar height m_tabLayout->setContentsMargins(12, 36, 12, 8); // Add +// top margin for titlebar + +// // Ensure the parent window attribute is maintained +// if (QMainWindow *mainWindow = qobject_cast(window())) { +// mainWindow->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, +// false); +// } + +// // Update glider position after titlebar integration changes +// if (m_currentIndex >= 0 && m_currentIndex < m_tabs.count()) { +// QTimer::singleShot(0, [this]() { +// animateGlider(m_currentIndex); +// }); +// } +// } +// #endif \ No newline at end of file diff --git a/src/customtabwidget.h b/src/customtabwidget.h new file mode 100644 index 0000000..327b1b6 --- /dev/null +++ b/src/customtabwidget.h @@ -0,0 +1,72 @@ +#ifndef CUSTOMTABWIDGET_H +#define CUSTOMTABWIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class CustomTab : public QPushButton +{ + Q_OBJECT + +public: + explicit CustomTab(const QString &text, QWidget *parent = nullptr); + void setNotificationCount(int count); + void setIcon(const QIcon &icon); + +private: + QLabel *m_notificationLabel; + int m_notificationCount; + void updateNotificationDisplay(); + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +class CustomTabWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CustomTabWidget(QWidget *parent = nullptr); + void finalizeStyles(); + int addTab(QWidget *widget, const QString &label); + int addTab(QWidget *widget, const QIcon &icon, const QString &label); + void setCurrentIndex(int index); + int currentIndex() const; + QWidget *widget(int index) const; + void setTabNotification(int index, int count); + +signals: + void currentChanged(int index); + +private slots: + void onTabClicked(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + QHBoxLayout *m_tabLayout; + QVBoxLayout *m_mainLayout; + QWidget *m_tabBar; + QStackedWidget *m_stackedWidget; + QButtonGroup *m_buttonGroup; + QWidget *m_glider; + QPropertyAnimation *m_gliderAnimation; + QList m_tabs; + QList m_widgets; + int m_currentIndex; + + void setupGlider(); + void animateGlider(int index); + void updateTabStyles(); +}; + +#endif // CUSTOMTABWIDGET_H \ No newline at end of file diff --git a/src/deviceinfowidget.cpp b/src/deviceinfowidget.cpp index 97c7efe..3a3df60 100644 --- a/src/deviceinfowidget.cpp +++ b/src/deviceinfowidget.cpp @@ -5,6 +5,7 @@ #include "iDescriptor-ui.h" #include "iDescriptor.h" #include +#include #include #include #include @@ -75,30 +76,50 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) QString::number(device->deviceInfo.diskInfo.totalDiskCapacity / (1000 * 1000 * 1000)) + " GB"); + + diskCapacityLabel->setAttribute(Qt::WA_StyledBackground, true); + diskCapacityLabel->setStyleSheet("background-color: rgba(0, 255, 30, 0.12);" + "padding: 4px;" + "border-radius: 4px;"); + m_chargingStatusLabel = new QLabel(device->deviceInfo.batteryInfo.isCharging ? "Charging" : "Not Charging"); - m_chargingStatusLabel->setStyleSheet("font-size: 1rem;"); + m_chargingStatusLabel->setStyleSheet( + device->deviceInfo.batteryInfo.isCharging ? "color: green;" + : "color: white;"); - m_chargingWattsLabel = - new QLabel(QString::number(device->deviceInfo.batteryInfo.watts) + "W"); - - m_cableTypeLabel = - new QLabel(device->deviceInfo.batteryInfo.usbConnectionType == - BatteryInfo::ConnectionType::USB - ? "USB" - : "USB-C"); + // Create the layout without a parent widget + QHBoxLayout *chargingLayout = new QHBoxLayout(); + chargingLayout->setContentsMargins(0, 0, 0, 0); + chargingLayout->setSpacing(5); + // Create icon label + m_lightningIconLabel = new QLabel(); + QPixmap lightningIcon(":/icons/MdiLightningBolt.png"); + QPixmap scaledIcon = lightningIcon.scaled(16, 16, Qt::KeepAspectRatio, + Qt::SmoothTransformation); + m_lightningIconLabel->setPixmap(scaledIcon); m_batteryWidget = new BatteryWidget(device->deviceInfo.batteryInfo.currentBatteryLevel, device->deviceInfo.batteryInfo.isCharging, this); + // Add the widgets to the new layout + chargingLayout->addWidget(m_chargingStatusLabel); + chargingLayout->addWidget(m_lightningIconLabel); + chargingLayout->addWidget(m_batteryWidget); + + m_chargingWattsWithCableTypeLabel = new QLabel( + QString::number(device->deviceInfo.batteryInfo.watts) + "W" + "/" + + (device->deviceInfo.batteryInfo.usbConnectionType == + BatteryInfo::ConnectionType::USB + ? "USB" + : "USB-C")); + headerLayout->addWidget(devProductType); headerLayout->addWidget(diskCapacityLabel); - headerLayout->addWidget(m_chargingStatusLabel); - headerLayout->addWidget(m_batteryWidget); - headerLayout->addWidget(m_chargingWattsLabel); - headerLayout->addWidget(m_cableTypeLabel); + headerLayout->addLayout(chargingLayout); + headerLayout->addWidget(m_chargingWattsWithCableTypeLabel); infoLayout->addWidget(headerWidget); // add spacer @@ -107,14 +128,47 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) // Add maximum stretch between header and grid infoLayout->addStretch(); - // Grid for device details + // --- Neumorphic Grid Widget --- + + // 1. Create a container for the shadows + QWidget *shadowContainer = new QWidget(); + // The container must be transparent to not hide the main window background + shadowContainer->setStyleSheet("background: transparent;"); + // Use a layout to make the gridWidget fill the container + QVBoxLayout *shadowLayout = new QVBoxLayout(shadowContainer); + shadowLayout->setContentsMargins(15, 15, 15, + 15); // Margins to make space for shadows + + // 2. Create the dark (bottom-right) shadow and apply to the container + QGraphicsDropShadowEffect *darkShadow = new QGraphicsDropShadowEffect(); + darkShadow->setBlurRadius(30); + darkShadow->setColor(QColor(0, 0, 0, 70)); // Dark, semi-transparent color + darkShadow->setOffset(0, 0); + shadowContainer->setGraphicsEffect(darkShadow); + + // 3. Create the grid widget (the main content) QWidget *gridWidget = new QWidget(); gridWidget->setObjectName("infoGrid"); - gridWidget->setStyleSheet("QWidget#infoGrid { " - " border: 1px solid #ccc; " - " border-radius: 6px; " - "}"); - QGridLayout *gridLayout = new QGridLayout(); + // Set a background color that matches the main window, with rounded corners + gridWidget->setStyleSheet( + "QWidget#infoGrid {" + " background-color: #2e2e2e;" // Match your window background + " border-radius: 8px;" + "}"); + + // 4. Create the light (top-left) shadow and apply to the grid widget + QGraphicsDropShadowEffect *lightShadow = new QGraphicsDropShadowEffect(); + lightShadow->setBlurRadius(30); + lightShadow->setColor( + QColor(255, 255, 255, 40)); // Light, semi-transparent color + lightShadow->setOffset(0, 0); + gridWidget->setGraphicsEffect(lightShadow); + + // Add gridWidget to the container's layout + shadowLayout->addWidget(gridWidget); + + QGridLayout *gridLayout = + new QGridLayout(gridWidget); // Set layout on gridWidget gridLayout->setSpacing(8); gridLayout->setColumnStretch(1, 1); // Allow value column to stretch gridLayout->setColumnStretch( @@ -228,7 +282,8 @@ DeviceInfoWidget::DeviceInfoWidget(iDescriptorDevice *device, QWidget *parent) } } - infoLayout->addWidget(gridWidget); + infoLayout->addWidget( + shadowContainer); // Add the container to the main layout // infoLayout->addStretch(); // Pushes footer to the bottom // Footer @@ -310,14 +365,27 @@ void DeviceInfoWidget::updateBatteryInfo() else parseDeviceBattery(ioreg, d); /*UI*/ - m_chargingStatusLabel->setText(d.batteryInfo.isCharging ? "Charging" - : "Not Charging"); - m_chargingWattsLabel->setText(QString::number(d.batteryInfo.watts) + "W"); - m_cableTypeLabel->setText(d.batteryInfo.usbConnectionType == - BatteryInfo::ConnectionType::USB - ? "USB" - : "USB-C"); + updateChargingStatusIcon(); + m_chargingWattsWithCableTypeLabel->setText( + QString::number(d.batteryInfo.watts) + "W" + "/" + + (d.batteryInfo.usbConnectionType == BatteryInfo::ConnectionType::USB + ? "USB" + : "USB-C")); m_batteryWidget->updateContext(d.batteryInfo.isCharging, d.batteryInfo.currentBatteryLevel); +} + +void DeviceInfoWidget::updateChargingStatusIcon() +{ + if (m_device->deviceInfo.batteryInfo.isCharging) { + m_chargingStatusLabel->setText("Charging"); + m_chargingStatusLabel->setStyleSheet("color: green;"); + m_lightningIconLabel->show(); + + } else { + m_chargingStatusLabel->setText("Not Charging"); + m_chargingStatusLabel->setStyleSheet("color: white;"); + m_lightningIconLabel->hide(); + } } \ No newline at end of file diff --git a/src/deviceinfowidget.h b/src/deviceinfowidget.h index 9b7a19b..22281bc 100644 --- a/src/deviceinfowidget.h +++ b/src/deviceinfowidget.h @@ -21,10 +21,11 @@ private: iDescriptorDevice *m_device; QTimer *m_updateTimer; void updateBatteryInfo(); + void updateChargingStatusIcon(); QLabel *m_chargingStatusLabel; - QLabel *m_chargingWattsLabel; - QLabel *m_cableTypeLabel; + QLabel *m_chargingWattsWithCableTypeLabel; BatteryWidget *m_batteryWidget; + QLabel *m_lightningIconLabel; }; #endif // DEVICEINFOWIDGET_H diff --git a/src/devicemenuwidget.cpp b/src/devicemenuwidget.cpp index 8bcc9c6..d16f974 100644 --- a/src/devicemenuwidget.cpp +++ b/src/devicemenuwidget.cpp @@ -5,37 +5,46 @@ #include "iDescriptor.h" #include "installedappswidget.h" #include -#include +#include #include DeviceMenuWidget::DeviceMenuWidget(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, device(device) { + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); - QWidget *centralWidget = new QWidget(this); - tabWidget = new QTabWidget(this); - tabWidget->tabBar()->hide(); - QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget); - mainLayout->addWidget(tabWidget); + stackedWidget = new QStackedWidget(this); + mainLayout->addWidget(stackedWidget); - FileExplorerWidget *explorer = new FileExplorerWidget(device, this); - explorer->setMinimumHeight(300); + // Create and add widgets to the stacked widget + DeviceInfoWidget *deviceInfoWidget = new DeviceInfoWidget(device, this); + InstalledAppsWidget *installedAppsWidget = + new InstalledAppsWidget(device, this); + GalleryWidget *galleryWidget = new GalleryWidget(device, this); + FileExplorerWidget *fileExplorerWidget = + new FileExplorerWidget(device, this); - GalleryWidget *gallery = new GalleryWidget(device, this); - gallery->setMinimumHeight(300); - setLayout(mainLayout); + // Set minimum heights + galleryWidget->setMinimumHeight(300); + fileExplorerWidget->setMinimumHeight(300); - tabWidget->addTab(new DeviceInfoWidget(device, this), ""); - tabWidget->addTab(new InstalledAppsWidget(device, this), ""); - unsigned int galleryIndex = tabWidget->addTab(gallery, ""); - tabWidget->addTab(explorer, ""); + // Add widgets to stack (index 0, 1, 2, 3) + stackedWidget->addWidget(deviceInfoWidget); // Index 0 - Info + stackedWidget->addWidget(installedAppsWidget); // Index 1 - Apps + stackedWidget->addWidget(galleryWidget); // Index 2 - Gallery + stackedWidget->addWidget(fileExplorerWidget); // Index 3 - Files - // TODO : one time ? - connect(tabWidget, &QTabWidget::currentChanged, this, - [this, galleryIndex, gallery](int index) { - if (index == galleryIndex) { + // Set default to Info tab + stackedWidget->setCurrentIndex(0); + + // Connect to current changed signal for lazy loading + connect(stackedWidget, &QStackedWidget::currentChanged, this, + [this, galleryWidget](int index) { + if (index == 2) { // Gallery tab qDebug() << "Switched to Gallery tab"; - gallery->load(); + galleryWidget->load(); } }); } @@ -43,13 +52,13 @@ DeviceMenuWidget::DeviceMenuWidget(iDescriptorDevice *device, QWidget *parent) void DeviceMenuWidget::switchToTab(const QString &tabName) { if (tabName == "Info") { - tabWidget->setCurrentIndex(0); + stackedWidget->setCurrentIndex(0); } else if (tabName == "Apps") { - tabWidget->setCurrentIndex(1); + stackedWidget->setCurrentIndex(1); } else if (tabName == "Gallery") { - tabWidget->setCurrentIndex(2); + stackedWidget->setCurrentIndex(2); } else if (tabName == "Files") { - tabWidget->setCurrentIndex(3); + stackedWidget->setCurrentIndex(3); } else { qDebug() << "Tab not found:" << tabName; } diff --git a/src/devicemenuwidget.h b/src/devicemenuwidget.h index c68e7d4..6363dbb 100644 --- a/src/devicemenuwidget.h +++ b/src/devicemenuwidget.h @@ -1,7 +1,7 @@ #ifndef DEVICEMENUWIDGET_H #define DEVICEMENUWIDGET_H #include "iDescriptor.h" -#include +#include #include class DeviceMenuWidget : public QWidget { @@ -12,8 +12,8 @@ public: void switchToTab(const QString &tabName); // ~DeviceMenuWidget(); private: - QTabWidget *tabWidget; // Pointer to the tab widget - iDescriptorDevice *device; // Pointer to the iDescriptor device + QStackedWidget *stackedWidget; // Pointer to the stacked widget + iDescriptorDevice *device; // Pointer to the iDescriptor device signals: }; diff --git a/src/devicesidebarwidget.cpp b/src/devicesidebarwidget.cpp index 79e28aa..4cd70d5 100644 --- a/src/devicesidebarwidget.cpp +++ b/src/devicesidebarwidget.cpp @@ -218,7 +218,7 @@ const std::string &DeviceSidebarItem::getDeviceUuid() const { return m_uuid; } DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(0); // Create scroll area diff --git a/src/iDescriptor-ui.h b/src/iDescriptor-ui.h index 025db7d..2eda016 100644 --- a/src/iDescriptor-ui.h +++ b/src/iDescriptor-ui.h @@ -1,5 +1,7 @@ #pragma once #include +#include + // A custom QGraphicsView that keeps the content fitted with aspect ratio on // resize class ResponsiveGraphicsView : public QGraphicsView @@ -18,4 +20,8 @@ protected: } QGraphicsView::resizeEvent(event); } -}; \ No newline at end of file +}; + +#ifdef Q_OS_MAC +void setupMacOSWindow(QMainWindow *window); +#endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7589afd..5867462 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "./ui_mainwindow.h" +#include "customtabwidget.h" #include "detailwindow.h" #include "settingswidget.h" #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include @@ -15,6 +17,7 @@ #include "appswidget.h" #include "devicemanagerwidget.h" +#include "iDescriptor-ui.h" #include "iDescriptor.h" #include "libirecovery.h" #include "toolboxwidget.h" @@ -24,6 +27,7 @@ #include #include #include +#include #include #include @@ -33,6 +37,10 @@ #include "fileexplorerwidget.h" #include "jailbrokenwidget.h" #include "recoverydeviceinfowidget.h" +#include +#include +#include +#include #include #include #include @@ -116,16 +124,67 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); - setWindowTitle("iDescriptor"); + + // Create custom tab widget + m_customTabWidget = new CustomTabWidget(this); + m_customTabWidget->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, + false); + + setContentsMargins(0, 0, 0, 0); +#ifdef Q_OS_MAC + setupMacOSWindow(this); + setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false); +#endif + setCentralWidget(m_customTabWidget); + + // Create device manager and stacked widget for main tab + m_mainStackedWidget = new QStackedWidget(); + + // No devices page + QWidget *noDevicesPage = new QWidget(); + QVBoxLayout *noDeviceLayout = new QVBoxLayout(noDevicesPage); + noDeviceLayout->addStretch(); + QHBoxLayout *labelLayout = new QHBoxLayout(); + labelLayout->addStretch(); + QLabel *noDeviceLabel = new QLabel("No devices detected"); + noDeviceLabel->setAlignment(Qt::AlignCenter); + labelLayout->addWidget(noDeviceLabel); + labelLayout->addStretch(); + noDeviceLayout->addLayout(labelLayout); + noDeviceLayout->addStretch(); m_deviceManager = new DeviceManagerWidget(this); - ui->stackedWidget->insertWidget(1, m_deviceManager); + + m_mainStackedWidget->addWidget(noDevicesPage); + m_mainStackedWidget->addWidget(m_deviceManager); + connect(m_deviceManager, &DeviceManagerWidget::updateNoDevicesConnected, this, &MainWindow::updateNoDevicesConnected); + // Add tabs with icons + QIcon deviceIcon(":/icons/MdiLightningBolt.png"); + m_customTabWidget->addTab(m_mainStackedWidget, deviceIcon, "iDevice"); + m_customTabWidget->addTab(new AppsWidget(this), "Apps"); + m_customTabWidget->addTab(new ToolboxWidget(this), "Toolbox"); + + auto *jailbrokenWidget = new JailbrokenWidget(this); + m_customTabWidget->addTab(jailbrokenWidget, "Jailbroken"); + m_customTabWidget->finalizeStyles(); + + // todo: is this ok ? + auto connection = std::make_shared(); + *connection = + connect(m_customTabWidget, &CustomTabWidget::currentChanged, this, + [this, jailbrokenWidget, connection](int index) { + if (index == 3) { // Jailbroken tab + jailbrokenWidget->initWidget(); + QObject::disconnect(*connection); + } + }); + // settings button QPushButton *settingsButton = new QPushButton(); - settingsButton->setIcon(QIcon::fromTheme("preferences-system")); + settingsButton->setIcon(QIcon(":/icons/MingcuteSettings7Line.png")); settingsButton->setToolTip("Settings"); settingsButton->setFlat(true); settingsButton->setCursor(Qt::PointingHandCursor); @@ -141,23 +200,23 @@ MainWindow::MainWindow(QWidget *parent) settingsDialog.setLayout(layout); settingsDialog.exec(); }); - // ui->centralwidget->layout()->addWidget(settingsButton); - ui->mainTabWidget->widget(1)->layout()->addWidget(new AppsWidget(this)); - ui->mainTabWidget->widget(2)->layout()->addWidget(new ToolboxWidget(this)); - auto *jailbrokenWidget = new JailbrokenWidget(this); - ui->mainTabWidget->widget(3)->layout()->addWidget(jailbrokenWidget); + m_connectedDeviceCountLabel = new QLabel("iDescriptor: no devices"); + m_connectedDeviceCountLabel->setContentsMargins(5, 0, 5, 0); + m_connectedDeviceCountLabel->setStyleSheet( + "QLabel:hover { background-color : #13131319; }"); - // TODO: is this a good idea? - auto connection = std::make_shared(); - *connection = connect(ui->mainTabWidget, &QTabWidget::currentChanged, this, - [this, jailbrokenWidget, connection](int index) { - if (index == 3) { // Jailbroken tab - jailbrokenWidget->initWidget(); - QObject::disconnect(*connection); - } - }); + ui->statusbar->addWidget(m_connectedDeviceCountLabel); + + ui->statusbar->setContentsMargins(0, 0, 0, 0); + + // QWidget *statusSpacer = new QWidget(); + // statusSpacer->setSizePolicy(QSizePolicy::Expanding, + // QSizePolicy::Preferred); + // statusSpacer->setAttribute(Qt::WA_TransparentForMouseEvents); + // ui->statusbar->addWidget(statusSpacer); ui->statusbar->addPermanentWidget(settingsButton); + irecv_error_t res_recovery = irecv_device_event_subscribe(&context, handleCallbackRecovery, nullptr); @@ -169,16 +228,38 @@ MainWindow::MainWindow(QWidget *parent) if (res != IDEVICE_E_SUCCESS) { printf("ERROR: Unable to subscribe to device events.\n"); } + createMenus(); +} + +void MainWindow::createMenus() +{ + QMenu *actionsMenu = menuBar()->addMenu("&Actions"); + + // Add a custom "About" action for your app + QAction *aboutAct = new QAction("&About iDescriptor", this); + connect(aboutAct, &QAction::triggered, this, [=]() { + QMessageBox::about(this, "About iDescriptor", + "iDescriptor
" + "A modern device management tool."); + }); + actionsMenu->addAction(aboutAct); } void MainWindow::updateNoDevicesConnected() { qDebug() << "Is there no devices connected? " << AppContext::sharedInstance()->noDevicesConnected(); - if (AppContext::sharedInstance()->noDevicesConnected()) - return ui->stackedWidget->setCurrentIndex( - 0); // Show "No Devices Connected" page - ui->stackedWidget->setCurrentIndex(1); // Show device list page + if (AppContext::sharedInstance()->noDevicesConnected()) { + + m_connectedDeviceCountLabel->setText("iDescriptor: no devices"); + return m_mainStackedWidget->setCurrentIndex( + 0); // Show "No Devices Connected" page + } + int deviceCount = AppContext::sharedInstance()->getConnectedDeviceCount(); + m_connectedDeviceCountLabel->setText( + "iDescriptor: " + QString::number(deviceCount) + + (deviceCount == 1 ? " device" : " devices") + " connected"); + m_mainStackedWidget->setCurrentIndex(1); // Show device list page } void MainWindow::onRecoveryDeviceAdded(QObject *recoveryDeviceInfoObj) @@ -187,7 +268,7 @@ void MainWindow::onRecoveryDeviceAdded(QObject *recoveryDeviceInfoObj) // TODO: handle return; try { - ui->stackedWidget->setCurrentIndex(1); + m_mainStackedWidget->setCurrentIndex(1); RecoveryDeviceInfo *device = qobject_cast(recoveryDeviceInfoObj); if (!device) { diff --git a/src/mainwindow.h b/src/mainwindow.h index c9d1e68..61f73f7 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,11 +1,12 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include "customtabwidget.h" #include "devicemanagerwidget.h" #include "devicemenuwidget.h" #include "iDescriptor.h" #include "libirecovery.h" +#include #include -#include QT_BEGIN_NAMESPACE namespace Ui @@ -21,21 +22,20 @@ class MainWindow : public QMainWindow public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); - -signals: - void deviceAdded(QString udid); // Signal for device connections + void onRecoveryDeviceAdded(QObject *recoveryDeviceInfoObj); + void onRecoveryDeviceRemoved(QObject *deviceInfoObj); public slots: - void onRecoveryDeviceAdded( - QObject *device_info); // Slot for recovery device connections - void onRecoveryDeviceRemoved( - QObject *device_info); // Slot for recovery device disconnections void onDeviceInitFailed(QString udid, lockdownd_error_t err); - -private: void updateNoDevicesConnected(); +private: + void createMenus(); + Ui::MainWindow *ui; - DeviceManagerWidget *m_deviceManager; // Add this member + CustomTabWidget *m_customTabWidget; + DeviceManagerWidget *m_deviceManager; + QStackedWidget *m_mainStackedWidget; + QLabel *m_connectedDeviceCountLabel; }; #endif // MAINWINDOW_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui index c380de1..692c31e 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -10,142 +10,6 @@ 600 - - MainWindow - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - iDevice - - - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - No devices detected - - - Qt::AlignmentFlag::AlignCenter - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - Apps - - - - - - Toolbox - - - - - - Jailbroken - - - - - - - - - - - - @@ -157,6 +21,8 @@ + + diff --git a/src/platform/macos.mm b/src/platform/macos.mm new file mode 100644 index 0000000..8ccbc77 --- /dev/null +++ b/src/platform/macos.mm @@ -0,0 +1,42 @@ +#include +#include +#include + +void setupMacOSWindow(QMainWindow *window) +{ + + if (!window) { + qWarning() << "setupMacOSWindow: window is null"; + return; + } + + NSView *nativeView = reinterpret_cast(window->winId()); + NSWindow *nativeWindow = [nativeView window]; + + if (!nativeWindow) { + qWarning() << "setupMacOSWindow: native window is null"; + return; + } + + qDebug() << "Setting up macOS window styles"; + + window->setUnifiedTitleAndToolBarOnMac(true); + + [nativeWindow setStyleMask:[nativeWindow styleMask] | + NSWindowStyleMaskFullSizeContentView | + NSWindowTitleHidden]; + [nativeWindow setTitleVisibility:NSWindowTitleHidden]; + [nativeWindow setTitlebarAppearsTransparent:YES]; + + NSToolbar *toolbar = + [[NSToolbar alloc] initWithIdentifier:@"HiddenInsetToolbar"]; + toolbar.showsBaselineSeparator = + NO; // equivalent to HideToolbarSeparator: true + [nativeWindow setToolbar:toolbar]; + // [toolbar setVisible:NO]; + // todo : is it ok ? + [toolbar release]; + // [nativeWindow setContentBorderThickness:0.0 forEdge:NSMinYEdge]; + + [nativeWindow center]; +} \ No newline at end of file diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index 9f77d2b..c4db591 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -71,6 +71,7 @@ ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} void ToolboxWidget::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(5, 5, 5, 5); // Device selection section QHBoxLayout *deviceLayout = new QHBoxLayout(); @@ -80,6 +81,7 @@ void ToolboxWidget::setupUI() deviceLayout->addWidget(m_deviceLabel); deviceLayout->addWidget(m_deviceCombo); + deviceLayout->setContentsMargins(0, 0, 0, 0); deviceLayout->addStretch(); mainLayout->addLayout(deviceLayout); @@ -88,6 +90,9 @@ void ToolboxWidget::setupUI() m_scrollArea = new QScrollArea(); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_scrollArea->setStyleSheet( + "QScrollArea { background: transparent; border: none; }"); + m_scrollArea->viewport()->setStyleSheet("background: transparent;"); m_contentWidget = new QWidget(); m_gridLayout = new QGridLayout(m_contentWidget); @@ -164,7 +169,7 @@ QWidget *ToolboxWidget::createToolbox(const QString &title, QFrame *frame = new QFrame(); frame->setObjectName("toolboxFrame"); frame->setFrameStyle(QFrame::Box); - frame->setStyleSheet("#toolboxFrame { border: 1px solid #ccc; " + frame->setStyleSheet("#toolboxFrame { " "border-radius: 5px; padding: 5px; }"); frame->setFixedSize(200, 120); @@ -261,13 +266,12 @@ void ToolboxWidget::updateToolboxStates() toolbox->setEnabled(enabled); if (enabled) { - toolbox->setStyleSheet("#toolboxFrame { border: 1px solid #ccc; " + toolbox->setStyleSheet("#toolboxFrame { " "border-radius: 5px; padding: 5px; }"); } else { - toolbox->setStyleSheet( - "#toolboxFrame { border: 1px solid #ccc; border-radius: 5px; " - "padding: " - "5px; background-color: #f0f0f0; color: #999; }"); + toolbox->setStyleSheet("#toolboxFrame { border-radius: 5px; " + "padding: 5px;" + "opacity: 0.45; }"); } } }