/* * 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 "toolboxwidget.h" #include "airplaywidget.h" #include "appcontext.h" #include "cableinfowidget.h" #include "devdiskimageswidget.h" #include "devdiskmanager.h" #include "iDescriptor-ui.h" #include "iDescriptor.h" #ifndef __APPLE__ #include "ifusewidget.h" #endif #include "livescreenwidget.h" #include "querymobilegestaltwidget.h" #include "virtuallocationwidget.h" #include "wirelessgalleryimportwidget.h" #include #include #include #include #ifdef WIN32 #include #endif struct iDescriptorToolWidget { iDescriptorTool tool; QString description; bool requiresDevice; QString iconName; }; ToolboxWidget *ToolboxWidget::sharedInstance() { static ToolboxWidget *instance = new ToolboxWidget(); return instance; } ToolboxWidget::ToolboxWidget(QWidget *parent) : QWidget{parent} { setupUI(); updateDeviceList(); updateToolboxStates(); connect(AppContext::sharedInstance(), &AppContext::deviceChange, this, &ToolboxWidget::updateUI); connect(AppContext::sharedInstance(), &AppContext::currentDeviceSelectionChanged, this, &ToolboxWidget::onCurrentDeviceChanged); } void ToolboxWidget::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); // Device selection section QHBoxLayout *deviceLayout = new QHBoxLayout(); m_deviceLabel = new QLabel("Device:"); m_deviceCombo = new QComboBox(); m_deviceCombo->setMinimumWidth(200); deviceLayout->addWidget(m_deviceLabel); deviceLayout->addWidget(m_deviceCombo); deviceLayout->setContentsMargins(15, 5, 15, 5); deviceLayout->addStretch(); mainLayout->addLayout(deviceLayout); // Scroll area for toolboxes 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(); QVBoxLayout *contentLayout = new QVBoxLayout(m_contentWidget); contentLayout->setSpacing(20); contentLayout->setContentsMargins(0, 0, 0, 0); // Main Tools Section QLabel *mainToolsLabel = new QLabel("Tools"); mainToolsLabel->setStyleSheet( "font-weight: bold; font-size: 14px; margin-left: 10px"); contentLayout->addWidget(mainToolsLabel); QWidget *mainToolsWidget = new QWidget(); m_gridLayout = new QGridLayout(mainToolsWidget); m_gridLayout->setSpacing(10); QList mainToolWidgets; mainToolWidgets.append( {iDescriptorTool::Airplayer, "Cast your device screen ", false, ""}); mainToolWidgets.append({iDescriptorTool::VirtualLocation, "Simulate GPS location on your device", true, ""}); mainToolWidgets.append({iDescriptorTool::LiveScreen, "View device screen in real-time", true, ""}); mainToolWidgets.append({iDescriptorTool::QueryMobileGestalt, "Query device hardware information", true, ""}); mainToolWidgets.append({iDescriptorTool::DeveloperDiskImages, "Manage developer disk images", false, ""}); mainToolWidgets.append( {iDescriptorTool::WirelessGalleryImport, "Import photos wirelessly to your iDevice (requires Shortcuts app)", false, ""}); #ifndef __APPLE__ mainToolWidgets.append({iDescriptorTool::iFuse, "Mount your iPhone's filesystem on your PC", true, ""}); #endif mainToolWidgets.append({iDescriptorTool::CableInfoWidget, "View detailed cable and connection info", true, ""}); mainToolWidgets.append({iDescriptorTool::NetworkDevices, "Discover and monitor devices on your network", false, ""}); for (int i = 0; i < mainToolWidgets.size(); ++i) { const auto &tool = mainToolWidgets[i]; ClickableWidget *toolbox = createToolbox(tool.tool, tool.description, tool.requiresDevice); int row = i / 3; int col = i % 3; m_gridLayout->addWidget(toolbox, row, col); } contentLayout->addWidget(mainToolsWidget); // More Tools Section QLabel *moreToolsLabel = new QLabel("More Tools"); moreToolsLabel->setStyleSheet( "font-weight: bold; font-size: 14px; margin-left: 10px"); contentLayout->addWidget(moreToolsLabel); QWidget *moreToolsWidget = new QWidget(); QGridLayout *moreGridLayout = new QGridLayout(moreToolsWidget); moreGridLayout->setSpacing(10); QList moreToolWidgets; moreToolWidgets.append( {iDescriptorTool::MountDevImage, "Mount a compatible device image with a single click", true, ""}); moreToolWidgets.append( {iDescriptorTool::Restart, "Restart device services", true, ""}); moreToolWidgets.append( {iDescriptorTool::Shutdown, "Shut down the device", true, ""}); moreToolWidgets.append({iDescriptorTool::RecoveryMode, "Enter device recovery mode", true, ""}); for (int i = 0; i < moreToolWidgets.size(); ++i) { const auto &tool = moreToolWidgets[i]; ClickableWidget *toolbox = createToolbox(tool.tool, tool.description, tool.requiresDevice); int row = i / 3; int col = i % 3; moreGridLayout->addWidget(toolbox, row, col); } contentLayout->addWidget(moreToolsWidget); contentLayout->addStretch(); m_scrollArea->setWidget(m_contentWidget); mainLayout->addWidget(m_scrollArea); connect(m_deviceCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ToolboxWidget::onDeviceSelectionChanged); } ClickableWidget *ToolboxWidget::createToolbox(iDescriptorTool tool, const QString &description, bool requiresDevice) { ClickableWidget *b = new ClickableWidget(); b->setStyleSheet("padding: 5px; border: none; outline: none;"); QVBoxLayout *layout = new QVBoxLayout(b); ZIconLabel *icon = new ZIconLabel(QIcon(), nullptr, 1.5, this); QString title; switch (tool) { case iDescriptorTool::Airplayer: title = "Airplayer"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLightAirplayOutline.png")); break; case iDescriptorTool::LiveScreen: title = "Live Screen"; icon->setIcon(QIcon(":/resources/icons/PepiconsPrintCellphoneEye.png")); break; case iDescriptorTool::MountDevImage: title = "Mount Dev Image"; icon->setIcon(QIcon(":/resources/icons/MdiDisk.png")); break; case iDescriptorTool::VirtualLocation: title = "Virtual Location"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLocationOnOutline.png")); break; case iDescriptorTool::Restart: title = "Restart"; icon->setIcon(QIcon(":/resources/icons/IcTwotoneRestartAlt.png")); break; case iDescriptorTool::Shutdown: title = "Shutdown"; icon->setIcon(QIcon(":/resources/icons/IcOutlinePowerSettingsNew.png")); break; case iDescriptorTool::RecoveryMode: title = "Recovery Mode"; icon->setIcon(QIcon(":/resources/icons/HugeiconsWrench01.png")); break; case iDescriptorTool::QueryMobileGestalt: title = "Query Mobile Gestalt"; icon->setIcon( QIcon(":/resources/icons/" "StreamlineProgrammingBrowserSearchSearchWindowGlassAppCod" "eProgrammingQueryFindMagnifyingApps.png")); break; case iDescriptorTool::DeveloperDiskImages: title = "Dev Disk Images"; icon->setIcon(QIcon(":/resources/icons/TablerDatabaseExport.png")); break; case iDescriptorTool::WirelessGalleryImport: title = "Wireless Gallery Import"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsAndroidWifi3BarPlus.png")); break; case iDescriptorTool::iFuse: title = "iFuse Mount"; icon->setIcon(QIcon(":/resources/icons/fuse.png")); icon->setIconThemable(false); break; case iDescriptorTool::CableInfoWidget: title = "Cable Info"; icon->setIcon( QIcon(":/resources/icons/MaterialSymbolsLightCableRounded.png")); break; case iDescriptorTool::NetworkDevices: title = "Network Devices"; icon->setIcon(QIcon( ":/resources/icons/StreamlineUltimateMultipleUsersNetwork.png")); break; default: title = "Unknown Tool"; break; } // Title QLabel *titleLabel = new QLabel(title); titleLabel->setAlignment(Qt::AlignCenter); // Description QLabel *descLabel = new QLabel(description); descLabel->setWordWrap(true); descLabel->setAlignment(Qt::AlignCenter); descLabel->setStyleSheet("color: #666; font-size: 12px;"); icon->setIconSizeMultiplier(1.90); layout->addWidget(icon, 0, Qt::AlignCenter); layout->addWidget(titleLabel); layout->addWidget(descLabel); b->setCursor(Qt::PointingHandCursor); m_toolboxes.append(b); b->setProperty("requiresDevice", requiresDevice); connect(b, &ClickableWidget::clicked, [this, tool, requiresDevice]() { onToolboxClicked(tool, requiresDevice); }); return b; } void ToolboxWidget::updateDeviceList() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); QList devices = AppContext::sharedInstance()->getAllDevices(); if (devices.isEmpty()) { m_deviceCombo->addItem("No device connected"); m_deviceCombo->setEnabled(false); m_uuid.clear(); } else { m_deviceCombo->setEnabled(true); for (iDescriptorDevice *device : devices) { QString shortUdid = QString::fromStdString(device->udid).left(8) + "..."; m_deviceCombo->addItem( QString::fromStdString(device->deviceInfo.productType) + " / " + shortUdid + (device->deviceInfo.isWireless ? " (Wi-Fi)" : ""), QString::fromStdString(device->udid)); } } onCurrentDeviceChanged( AppContext::sharedInstance()->getCurrentDeviceSelection()); m_deviceCombo->blockSignals(false); if (m_deviceCombo->count() > 0 && m_deviceCombo->currentIndex() >= 0) { QString currentUdid = m_deviceCombo->currentData().toString(); if (!currentUdid.isEmpty()) { m_uuid = currentUdid.toStdString(); qDebug() << "[toolboxwidget] Initialized m_uuid to:" << currentUdid; } } } void ToolboxWidget::updateToolboxStates() { bool hasDevice = !AppContext::sharedInstance()->getAllDevices().isEmpty(); for (int i = 0; i < m_toolboxes.size(); ++i) { QWidget *toolbox = m_toolboxes[i]; bool requiresDevice = toolbox->property("requiresDevice").toBool(); bool enabled = !requiresDevice || hasDevice; toolbox->setEnabled(enabled); // Opacity does not work because of the stylesheet on Windows #ifndef WIN32 if (enabled) { toolbox->setStyleSheet("QWidget#toolboxFrame { " "padding: 5px; }"); } else { toolbox->setStyleSheet("QWidget#toolboxFrame { " "padding: 5px;" "opacity: 0.45; }"); } #else // base style // toolbox->setStyleSheet("QWidget#toolboxFrame{ padding: 5px; border: " // "none; outline: none; }"); if (enabled) { // normal look toolbox->setStyleSheet("QWidget#toolboxFrame { padding: 5px; " "border: none; outline: none; }"); toolbox->setCursor(Qt::PointingHandCursor); } else { // disabled look: dull bg + border + muted text, no hand cursor toolbox->setStyleSheet("padding: 5px;" "border-radius: 8px;" "background-color: rgba(255,255,255,1);" "color: #666;"); toolbox->setCursor(Qt::ArrowCursor); } #endif } } void ToolboxWidget::updateUI() { updateDeviceList(); updateToolboxStates(); } void ToolboxWidget::onDeviceSelectionChanged() { QString selectedUdid = m_deviceCombo->currentData().toString(); // Clear m_uuid if no valid selection if (selectedUdid.isEmpty()) { m_uuid.clear(); return; } if (AppContext::sharedInstance()->getDevice(selectedUdid.toStdString()) == nullptr) { QMessageBox::warning(this, "Device Not Found", "The selected device is no longer connected."); m_uuid.clear(); // Clear stale UUID updateDeviceList(); return; } m_uuid = selectedUdid.toStdString(); qDebug() << "[toolboxwidget] Selected device UDID:" << selectedUdid; // Update the selected device in main menu AppContext::sharedInstance()->setCurrentDeviceSelection( DeviceSelection(selectedUdid.toStdString())); } void ToolboxWidget::onCurrentDeviceChanged(const DeviceSelection &selection) { if (selection.valid() && selection.type == DeviceSelection::Normal) { int index = m_deviceCombo->findData(QString::fromStdString(selection.udid)); if (index != -1) { // Block signals to prevent recursive calls when we update the UI m_deviceCombo->blockSignals(true); m_deviceCombo->setCurrentIndex(index); m_deviceCombo->blockSignals(false); m_uuid = selection.udid; } } else { // Clear m_uuid when selection is invalid m_uuid.clear(); } } void ToolboxWidget::onToolboxClicked(iDescriptorTool tool, bool requiresDevice) { // final check to make sure device is connected if required iDescriptorDevice *device = AppContext::sharedInstance()->getDevice(m_uuid); if (!device && requiresDevice) { QMessageBox::warning( this, "Device Disconnected ?", "Device just disconnected, please select a device."); return; } qDebug() << "idevice exists:" << (device != nullptr) << m_uuid.c_str(); switch (tool) { case iDescriptorTool::Airplayer: { if (!m_airplayWidget) { m_airplayWidget = new AirPlayWidget(); connect(m_airplayWidget, &QObject::destroyed, this, [this]() { m_airplayWidget = nullptr; }); m_airplayWidget->setAttribute(Qt::WA_DeleteOnClose); m_airplayWidget->setWindowFlag(Qt::Window); m_airplayWidget->resize(400, 300); m_airplayWidget->show(); } else { m_airplayWidget->raise(); m_airplayWidget->activateWindow(); } } break; case iDescriptorTool::LiveScreen: { LiveScreenWidget *liveScreen = new LiveScreenWidget(device); liveScreen->show(); } break; case iDescriptorTool::RecoveryMode: { enterRecoveryMode(device); } break; case iDescriptorTool::MountDevImage: { DevDiskImageHelper *devDiskImageHelper = new DevDiskImageHelper(device, this); connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, this, [this, devDiskImageHelper](bool success) { devDiskImageHelper->deleteLater(); if (success) { QMessageBox::information( this, "Success", "Developer image mounted successfully."); } else { QMessageBox::warning( this, "Failure", "Failed to mount developer image."); } }); devDiskImageHelper->start(); } break; case iDescriptorTool::VirtualLocation: { VirtualLocation *virtualLocation = new VirtualLocation(device); virtualLocation->setAttribute(Qt::WA_DeleteOnClose); virtualLocation->show(); } break; case iDescriptorTool::Restart: { restartDevice(device); } break; case iDescriptorTool::Shutdown: { shutdownDevice(device); } break; case iDescriptorTool::QueryMobileGestalt: { QueryMobileGestaltWidget *queryMobileGestaltWidget = new QueryMobileGestaltWidget(device); queryMobileGestaltWidget->setAttribute(Qt::WA_DeleteOnClose); queryMobileGestaltWidget->show(); } break; case iDescriptorTool::DeveloperDiskImages: { if (!m_devDiskImagesWidget) { m_devDiskImagesWidget = new DevDiskImagesWidget(device); m_devDiskImagesWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_devDiskImagesWidget, &QObject::destroyed, this, [this]() { m_devDiskImagesWidget = nullptr; }); m_devDiskImagesWidget->show(); } else { m_devDiskImagesWidget->raise(); m_devDiskImagesWidget->activateWindow(); } } break; case iDescriptorTool::WirelessGalleryImport: { if (!m_wirelessGalleryImportWidget) { m_wirelessGalleryImportWidget = new WirelessGalleryImportWidget(); connect(m_wirelessGalleryImportWidget, &QObject::destroyed, this, [this]() { m_wirelessGalleryImportWidget = nullptr; }); m_wirelessGalleryImportWidget->setAttribute(Qt::WA_DeleteOnClose); m_wirelessGalleryImportWidget->show(); } else { m_wirelessGalleryImportWidget->raise(); m_wirelessGalleryImportWidget->activateWindow(); } } break; #ifndef __APPLE__ case iDescriptorTool::iFuse: { if (!m_ifuseWidget) { m_ifuseWidget = new iFuseWidget(device); qDebug() << "Created iFuseWidget" << device->deviceInfo.productType.c_str(); m_ifuseWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_ifuseWidget, &QObject::destroyed, this, [this]() { m_ifuseWidget = nullptr; }); m_ifuseWidget->setWindowFlag(Qt::Window); m_ifuseWidget->resize(600, 400); m_ifuseWidget->show(); } else { m_ifuseWidget->raise(); m_ifuseWidget->activateWindow(); } } break; #endif case iDescriptorTool::CableInfoWidget: { CableInfoWidget *cableInfoWidget = new CableInfoWidget(device); cableInfoWidget->show(); } break; case iDescriptorTool::NetworkDevices: { if (!m_networkDevicesWidget) { m_networkDevicesWidget = new NetworkDevicesWidget(); m_networkDevicesWidget->setAttribute(Qt::WA_DeleteOnClose); connect(m_networkDevicesWidget, &QObject::destroyed, this, [this]() { m_networkDevicesWidget = nullptr; }); m_networkDevicesWidget->show(); } else { m_networkDevicesWidget->raise(); m_networkDevicesWidget->activateWindow(); } } break; default: qDebug() << "Clicked on unimplemented tool"; break; } } void ToolboxWidget::restartDevice(const iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Restart Device"); msgBox.setText("Are you sure you want to restart the device?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } // FIXME: move to servicemanager auto res = device->diagRelay->restart(); if (res.is_err()) { QMessageBox::warning( nullptr, "Restart Failed", "Failed to restart device: " + QString::fromStdString(res.unwrap_err().message)); } else { QMessageBox::information(nullptr, "Restart Initiated", "Device will restart once unplugged."); qDebug() << "Restarting device"; } } void ToolboxWidget::shutdownDevice(const iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Shutdown Device"); msgBox.setText("Are you sure you want to shutdown the device?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } // FIXME: move to servicemanager auto res = device->diagRelay->shutdown(); if (res.is_err()) { QMessageBox::warning( nullptr, "Shutdown Failed", "Failed to shutdown device: " + QString::fromStdString(res.unwrap_err().message)); } else { QMessageBox::information(nullptr, "Shutdown Initiated", "Device will shutdown once unplugged."); qDebug() << "Shutting down device"; } } void ToolboxWidget::enterRecoveryMode(const iDescriptorDevice *device) { QMessageBox msgBox; msgBox.setWindowTitle("Enter Recovery Mode"); msgBox.setText("Are you sure you want to enter recovery mode?"); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); msgBox.setDefaultButton(QMessageBox::No); int ret = msgBox.exec(); if (ret != QMessageBox::Yes) return; if (!device || device->udid.empty()) { return; } IdeviceFfiError *error = lockdownd_enter_recovery(device->lockdown); if (error != nullptr) { QMessageBox::warning(nullptr, "Enter Recovery Mode Failed", "Failed to enter recovery mode: " + QString::fromStdString(error->message)); idevice_error_free(error); } else { QMessageBox::information(nullptr, "Enter Recovery Mode Initiated", "Device will enter recovery mode."); } } void ToolboxWidget::restartAirPlayWidget() { if (!m_airplayWidget) { onToolboxClicked(iDescriptorTool::Airplayer, false); return; } connect( m_airplayWidget, &QObject::destroyed, this, [this]() { // give some time for cleanup QTimer::singleShot(100, this, [this]() { onToolboxClicked(iDescriptorTool::Airplayer, false); }); }, Qt::SingleShotConnection); m_airplayWidget->close(); }