/* * iDescriptor: A free and open-source idevice management tool. * * Copyright (C) 2025 Uncore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "devicesidebarwidget.h" #include "appcontext.h" #include "iDescriptor-ui.h" #include "loadingspinnerwidget.h" #include "qprocessindicator.h" #include // DeviceSidebarItem Implementation DeviceSidebarItem::DeviceSidebarItem(const QString &deviceName, const std::string &uuid, QWidget *parent) : QFrame(parent), m_deviceName(deviceName), m_uuid(uuid), m_selected(false), m_collapsed(false) { setupUI(); setFrameStyle(QFrame::StyledPanel); setLineWidth(1); updateToggleButton(); } void DeviceSidebarItem::setupUI() { m_mainLayout = new QVBoxLayout(this); m_mainLayout->setContentsMargins(5, 5, 5, 5); m_mainLayout->setSpacing(5); // Header section (always visible) m_headerWidget = new ClickableWidget(); QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget); headerLayout->setContentsMargins(0, 0, 0, 0); headerLayout->setSpacing(2); connect(m_headerWidget, &ClickableWidget::clicked, this, [this]() { emit deviceSelected(m_uuid); }); // Device name label m_deviceLabel = new QLabel(m_deviceName); m_deviceLabel->setStyleSheet("QLabel { font-weight: bold; }"); m_deviceLabel->setWordWrap(true); headerLayout->addWidget(m_deviceLabel); // Toggle button m_toggleButton = new QPushButton(); m_toggleButton->setFlat(true); m_toggleButton->setMaximumHeight(20); m_toggleButton->setStyleSheet("QPushButton { " " text-align: left; " " padding: 2px 5px; " " border: none; " " color: #666; " " font-size: 11px; " "} " "QPushButton:hover { " " background-color: #f0f0f0; " " border-radius: 3px; " "}"); connect(m_toggleButton, &QPushButton::clicked, this, &DeviceSidebarItem::onToggleCollapse); headerLayout->addWidget(m_toggleButton); m_mainLayout->addWidget(m_headerWidget); // Options section (collapsible) m_optionsWidget = new QWidget(); QVBoxLayout *optionsLayout = new QVBoxLayout(m_optionsWidget); optionsLayout->setContentsMargins(5, 5, 5, 5); optionsLayout->setSpacing(3); // Create navigation buttons m_infoButton = new QPushButton("Info"); m_appsButton = new QPushButton("Apps"); m_galleryButton = new QPushButton("Gallery"); m_filesButton = new QPushButton("Files"); // Create button group for exclusive selection m_navigationGroup = new QButtonGroup(this); m_navigationGroup->addButton(m_infoButton, 0); m_navigationGroup->addButton(m_appsButton, 1); m_navigationGroup->addButton(m_galleryButton, 2); m_navigationGroup->addButton(m_filesButton, 3); // Style the navigation buttons QList navButtons = {m_infoButton, m_appsButton, m_galleryButton, m_filesButton}; for (QPushButton *btn : navButtons) { btn->setCheckable(true); btn->setMaximumHeight(25); btn->setStyleSheet( QString("QPushButton { " " background-color: #f8f9fa; " " border: 1px solid #dee2e6; " " padding: 4px 8px; " " text-align: center; " " border-radius: 3px; " " font-size: 11px; " " color: #212529; " "} " "QPushButton:checked { " " background-color: %1; " " color: white; " " border: 1px solid %1; " "} " "QPushButton:hover:!checked { " " background-color: #e9ecef; " " border-color: #adb5bd; " "} " "QPushButton:checked:hover { " " background-color: %2; " "}") .arg(COLOR_ACCENT_BLUE.name(), COLOR_BLUE.name())); connect(btn, &QPushButton::clicked, this, &DeviceSidebarItem::onNavigationButtonClicked); optionsLayout->addWidget(btn); } // Set default selection m_infoButton->setChecked(true); m_mainLayout->addWidget(m_optionsWidget); // Initialize UI state to match m_collapsed value // This ensures consistency regardless of initial m_collapsed value updateToggleButton(); toggleCollapse(); setSelected(false); } void DeviceSidebarItem::setSelected(bool selected) { m_selected = selected; // todo : bug the first device selected style is not applied if (selected) { setStyleSheet(QString("DeviceSidebarItem { border: " "2px solid %1; border-radius: 5px; }") .arg(COLOR_BLUE.name())); } else { setStyleSheet("DeviceSidebarItem { border: " "1px solid #e0e0e0; border-radius: 5px; }"); } } void DeviceSidebarItem::setCollapsed(bool collapsed) { if (m_collapsed == collapsed) return; m_collapsed = collapsed; updateToggleButton(); toggleCollapse(); } void DeviceSidebarItem::updateToggleButton() { if (m_collapsed) { m_toggleButton->setText("▶ Options"); } else { m_toggleButton->setText("▼ Options"); } } void DeviceSidebarItem::toggleCollapse() { if (m_collapsed) { m_optionsWidget->hide(); m_optionsWidget->setMaximumHeight(0); } else { m_optionsWidget->show(); m_optionsWidget->setMaximumHeight(QWIDGETSIZE_MAX); } } void DeviceSidebarItem::onToggleCollapse() { setCollapsed(!m_collapsed); } void DeviceSidebarItem::onNavigationButtonClicked() { QPushButton *button = qobject_cast(sender()); if (button) { // Only emit navigationRequested - this will handle both device // selection and tab switching emit navigationRequested(m_uuid, button->text()); // Remove this line: emit deviceSelected(m_uuid); } } const std::string &DeviceSidebarItem::getDeviceUuid() const { return m_uuid; } RecoveryDeviceSidebarItem::RecoveryDeviceSidebarItem(uint64_t ecid, QWidget *parent) : QFrame(parent), m_ecid(ecid) { setupUI(); } void RecoveryDeviceSidebarItem::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(5, 5, 5, 5); mainLayout->setSpacing(5); ClickableWidget *headerWidget = new ClickableWidget(); connect(headerWidget, &ClickableWidget::clicked, this, [this]() { emit recoveryDeviceSelected(m_ecid); }); QVBoxLayout *headerLayout = new QVBoxLayout(headerWidget); headerLayout->setContentsMargins(0, 0, 0, 0); QLabel *titleLabel = new QLabel("Recovery Mode"); titleLabel->setStyleSheet("QLabel { font-weight: bold; }"); titleLabel->setWordWrap(true); headerLayout->addWidget(titleLabel); mainLayout->addWidget(headerWidget); setStyleSheet("RecoveryDeviceSidebarItem { border: " "1px solid #e0e0e0; border-radius: 5px; }"); } void RecoveryDeviceSidebarItem::setSelected(bool selected) { if (m_selected == selected) return; m_selected = selected; if (selected) { setStyleSheet("RecoveryDeviceSidebarItem { border: " "2px solid #2196f3; border-radius: 5px; }"); } else { setStyleSheet("RecoveryDeviceSidebarItem { border: " "1px solid #e0e0e0; border-radius: 5px; }"); } } // DeviceSidebarWidget Implementation DeviceSidebarWidget::DeviceSidebarWidget(QWidget *parent) : QWidget(parent), m_currentSelection(DeviceSelection("")) { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(0); // Create scroll area m_scrollArea = new QScrollArea(); m_scrollArea->setWidgetResizable(true); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_scrollArea->setFrameStyle(QFrame::NoFrame); // Make the scroll area and its viewport transparent m_scrollArea->setStyleSheet( "QScrollArea { background: transparent; border: none; }"); m_scrollArea->viewport()->setStyleSheet("background: transparent;"); // Create content widget m_contentWidget = new QWidget(); m_contentLayout = new QVBoxLayout(m_contentWidget); m_contentLayout->setContentsMargins(5, 5, 5, 5); m_contentLayout->setSpacing(10); m_contentLayout->addStretch(); // Push items to top // Ensure the content widget is also transparent m_contentWidget->setStyleSheet("background: transparent;"); m_scrollArea->setWidget(m_contentWidget); mainLayout->addWidget(m_scrollArea); // Set minimum width setMinimumWidth(200); setMaximumWidth(200); // Listen to AppContext selection changes connect(AppContext::sharedInstance(), &AppContext::currentDeviceSelectionChanged, this, &DeviceSidebarWidget::setCurrentSelection); } DeviceSidebarItem *DeviceSidebarWidget::addDevice(const QString &deviceName, const std::string &uuid) { DeviceSidebarItem *item = new DeviceSidebarItem(deviceName, uuid, this); // Connect to unified handler connect(item, &DeviceSidebarItem::deviceSelected, this, [this](const std::string &uuid) { onItemSelected(DeviceSelection(uuid)); }); connect(item, &DeviceSidebarItem::navigationRequested, this, [this](const std::string &uuid, const QString §ion) { onItemSelected(DeviceSelection(uuid, section)); }); m_deviceItems[uuid] = item; m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); return item; } DevicePendingSidebarItem * DeviceSidebarWidget::addPendingDevice(const QString &uuid) { DevicePendingSidebarItem *item = new DevicePendingSidebarItem(uuid, this); connect(item, &DevicePendingSidebarItem::clicked, this, [this, uuid]() { onItemSelected(DeviceSelection::pending(uuid.toStdString())); }); m_pendingItems[uuid.toStdString()] = item; m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); return item; } RecoveryDeviceSidebarItem *DeviceSidebarWidget::addRecoveryDevice(uint64_t ecid) { RecoveryDeviceSidebarItem *item = new RecoveryDeviceSidebarItem(ecid, this); // Connect to unified handler connect(item, &RecoveryDeviceSidebarItem::recoveryDeviceSelected, this, [this](uint64_t ecid) { onItemSelected(DeviceSelection(ecid)); }); m_recoveryItems[ecid] = item; m_contentLayout->insertWidget(m_contentLayout->count() - 1, item); return item; } void DeviceSidebarWidget::removeDevice(const std::string &uuid) { if (m_deviceItems.contains(uuid)) { DeviceSidebarItem *item = m_deviceItems[uuid]; m_deviceItems.remove(uuid); m_contentLayout->removeWidget(item); item->deleteLater(); } } void DeviceSidebarWidget::removePendingDevice(const std::string &uuid) { if (m_pendingItems.contains(uuid)) { DevicePendingSidebarItem *item = m_pendingItems[uuid]; m_pendingItems.remove(uuid); m_contentLayout->removeWidget(item); item->deleteLater(); } } void DeviceSidebarWidget::removeRecoveryDevice(uint64_t ecid) { if (m_recoveryItems.contains(ecid)) { RecoveryDeviceSidebarItem *item = m_recoveryItems[ecid]; m_recoveryItems.remove(ecid); m_contentLayout->removeWidget(item); item->deleteLater(); } } void DeviceSidebarWidget::setCurrentSelection(const DeviceSelection &selection) { m_currentSelection = selection; updateSelection(); } void DeviceSidebarWidget::onItemSelected(const DeviceSelection &selection) { AppContext::sharedInstance()->setCurrentDeviceSelection(selection); } void DeviceSidebarWidget::updateSelection() { // Clear all selections first for (auto item : m_deviceItems) { item->setSelected(false); } for (auto item : m_recoveryItems) { item->setSelected(false); } for (auto item : m_pendingItems) { item->setSelected(false); } // Set selection based on current selection if (m_currentSelection.type == DeviceSelection::Normal && m_deviceItems.contains(m_currentSelection.udid)) { m_deviceItems[m_currentSelection.udid]->setSelected(true); } else if (m_currentSelection.type == DeviceSelection::Recovery && m_recoveryItems.contains(m_currentSelection.ecid)) { m_recoveryItems[m_currentSelection.ecid]->setSelected(true); } else if (m_currentSelection.type == DeviceSelection::Pending && m_pendingItems.contains(m_currentSelection.udid)) { m_pendingItems[m_currentSelection.udid]->setSelected(true); } } DevicePendingSidebarItem::DevicePendingSidebarItem(const QString &udid, QWidget *parent) : QFrame(parent), m_udid(udid) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setContentsMargins(10, 10, 10, 10); layout->setSpacing(1); QProcessIndicator *spinner = new QProcessIndicator(this); spinner->setFixedSize(26, 26); spinner->setType(QProcessIndicator::line_rotate); spinner->start(); QLabel *label = new QLabel("Pairing...", this); layout->addWidget(label); layout->addWidget(spinner); setLayout(layout); setSelected(false); } void DevicePendingSidebarItem::setSelected(bool selected) { m_selected = selected; if (selected) { setStyleSheet(QString("DevicePendingSidebarItem { border: " "2px solid %1; border-radius: 5px; }") .arg(COLOR_BLUE.name())); } else { setStyleSheet("DevicePendingSidebarItem { border: " "1px solid #e0e0e0; border-radius: 5px; }"); } } void DevicePendingSidebarItem::mousePressEvent(QMouseEvent *event) { emit clicked(); QFrame::mousePressEvent(event); }