Files
iDescriptor/src/devicesidebarwidget.cpp
T

447 lines
15 KiB
C++

/*
* iDescriptor: A free and open-source idevice management tool.
*
* Copyright (C) 2025 Uncore <https://github.com/uncor3>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "devicesidebarwidget.h"
#include "appcontext.h"
#include "iDescriptor-ui.h"
#include "loadingspinnerwidget.h"
#include "qprocessindicator.h"
#include <QDebug>
// 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<QPushButton *> 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<QPushButton *>(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 &section) {
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);
}