/* * 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 "virtuallocationwidget.h" #include "appcontext.h" #include "devdiskimagehelper.h" #include "devdiskmanager.h" #include "iDescriptor.h" #include "settingsmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include VirtualLocation::VirtualLocation(iDescriptorDevice *device, QWidget *parent) : QWidget{parent}, m_device(device) { unsigned int device_version = idevice_get_device_version(m_device->device); unsigned int deviceMajorVersion = (device_version >> 16) & 0xFF; if (deviceMajorVersion > 16) { QMessageBox::warning( this, "Unsupported iOS Version", "Virtual Location feature requires iOS 16 or earlier.\n" "Your device is running iOS " + QString::number(deviceMajorVersion) + ", which is not yet supported."); QTimer::singleShot(0, this, &QWidget::close); return; } setWindowTitle("Virtual Location - iDescriptor"); // Create the main layout QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setSpacing(10); // Create left panel for controls QWidget *rightPanel = new QWidget(); rightPanel->setFixedWidth(250); m_rightLayout = new QVBoxLayout(rightPanel); m_rightLayout->setContentsMargins(15, 15, 15, 15); m_rightLayout->setSpacing(10); // Title QLabel *titleLabel = new QLabel("Virtual Location Settings"); titleLabel->setStyleSheet("margin-bottom: 10px;"); m_rightLayout->addWidget(titleLabel); QGroupBox *coordGroup = new QGroupBox("Coordinates"); m_rightLayout->addWidget(coordGroup); QVBoxLayout *coordLayout = new QVBoxLayout(coordGroup); // Latitude input QLabel *latLabel = new QLabel("Latitude:"); coordLayout->addWidget(latLabel); m_latitudeEdit = new QLineEdit(); m_latitudeEdit->setPlaceholderText("e.g., 59.9139"); m_latitudeEdit->setText("59.9139"); m_latitudeEdit->setValidator(new QDoubleValidator(-90.0, 90.0, 6, this)); coordLayout->addWidget(m_latitudeEdit); // Longitude input QLabel *lonLabel = new QLabel("Longitude:"); coordLayout->addWidget(lonLabel); m_longitudeEdit = new QLineEdit(); m_longitudeEdit->setPlaceholderText("e.g., 10.7522"); m_longitudeEdit->setText("10.7522"); m_longitudeEdit->setValidator(new QDoubleValidator(-180.0, 180.0, 6, this)); coordLayout->addWidget(m_longitudeEdit); // Add some spacing m_rightLayout->addItem( new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Fixed)); // Apply button m_applyButton = new QPushButton("Apply Location"); m_applyButton->setDefault(true); m_rightLayout->addWidget(m_applyButton); // Recent locations section loadRecentLocations(m_rightLayout); // Add stretch to push everything to the top m_rightLayout->addStretch(); // Connect to recent locations changes connect(SettingsManager::sharedInstance(), &SettingsManager::recentLocationsChanged, this, &VirtualLocation::refreshRecentLocations); // Create map widget m_quickWidget = new QQuickWidget(this); m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); m_quickWidget->setSource(QUrl(QStringLiteral("qrc:/qml/MapView.qml"))); // Enable input handling m_quickWidget->setFocusPolicy(Qt::StrongFocus); m_quickWidget->setAttribute(Qt::WA_AcceptTouchEvents, true); // Add widgets to main layout mainLayout->addWidget(m_quickWidget, 1); // Give map widget stretch factor of 1 mainLayout->addWidget(rightPanel); setLayout(mainLayout); // Connect signals connect(m_latitudeEdit, &QLineEdit::textChanged, this, &VirtualLocation::onInputChanged); connect(m_longitudeEdit, &QLineEdit::textChanged, this, &VirtualLocation::onInputChanged); connect(m_applyButton, &QPushButton::clicked, this, &VirtualLocation::onApplyClicked); // Connect to QML map connect(m_quickWidget, &QQuickWidget::statusChanged, this, &VirtualLocation::onQuickWidgetStatusChanged); // Register this object with QML context so QML can call our slots m_quickWidget->rootContext()->setContextProperty("cppHandler", this); qDebug() << "QuickWidget status:" << m_quickWidget->status(); qDebug() << "QuickWidget errors:" << m_quickWidget->errors(); connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this, [this](const std::string &udid) { if (m_device->udid == udid) { this->close(); this->deleteLater(); } }); // DevDiskManager::sharedInstance()->downloadCompatibleImage(m_device); } void VirtualLocation::onQuickWidgetStatusChanged(QQuickWidget::Status status) { if (status == QQuickWidget::Ready) { qDebug() << "QuickWidget is ready"; // Set initial map position updateMapFromInputs(); } else if (status == QQuickWidget::Error) { qDebug() << "QuickWidget errors:" << m_quickWidget->errors(); } } void VirtualLocation::onInputChanged() { // Update map when input changes (with slight delay to avoid too frequent // updates) m_updateTimer.setSingleShot(true); m_updateTimer.setInterval(500); // 500ms delay disconnect(&m_updateTimer, &QTimer::timeout, this, &VirtualLocation::updateMapFromInputs); connect(&m_updateTimer, &QTimer::timeout, this, &VirtualLocation::updateMapFromInputs); m_updateTimer.start(); } void VirtualLocation::updateMapFromInputs() { bool latOk, lonOk; double latitude = m_latitudeEdit->text().toDouble(&latOk); double longitude = m_longitudeEdit->text().toDouble(&lonOk); if (latOk && lonOk && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180) { QQuickItem *rootObject = m_quickWidget->rootObject(); if (rootObject) { QQuickItem *mapItem = rootObject->findChild("map"); if (mapItem) { // Block signals to prevent feedback loop m_updatingFromInput = true; // Call QML function to update map center QMetaObject::invokeMethod(mapItem, "updateCenter", Q_ARG(QVariant, latitude), Q_ARG(QVariant, longitude)); m_updatingFromInput = false; qDebug() << "Updated map center to:" << latitude << "," << longitude; } } } } void VirtualLocation::onMapCenterChanged() { if (m_updatingFromInput) { return; // Prevent feedback loop } qDebug() << "onMapCenterChanged called!"; QQuickItem *rootObject = m_quickWidget->rootObject(); if (rootObject) { QQuickItem *mapItem = rootObject->findChild("map"); if (mapItem) { // Get map center using QMetaObject::invokeMethod for more reliable // access QVariant centerVar = mapItem->property("center"); if (centerVar.isValid()) { // Try to get the coordinate directly QGeoCoordinate coord = centerVar.value(); if (coord.isValid()) { double latitude = coord.latitude(); double longitude = coord.longitude(); // Block signals temporarily to prevent feedback m_latitudeEdit->blockSignals(true); m_longitudeEdit->blockSignals(true); // Update input fields m_latitudeEdit->setText(QString::number(latitude, 'f', 6)); m_longitudeEdit->setText( QString::number(longitude, 'f', 6)); // Restore signals m_latitudeEdit->blockSignals(false); m_longitudeEdit->blockSignals(false); qDebug() << "Updated inputs from map:" << latitude << "," << longitude; } else { qDebug() << "Invalid coordinate from map"; } } else { qDebug() << "Could not get center property from map"; } } } } // Add this new slot that QML can call directly void VirtualLocation::updateInputsFromMap(double latitude, double longitude) { if (m_updatingFromInput) { return; // Prevent feedback loop } qDebug() << "updateInputsFromMap called with:" << latitude << "," << longitude; // Block signals temporarily to prevent feedback m_latitudeEdit->blockSignals(true); m_longitudeEdit->blockSignals(true); // Update input fields m_latitudeEdit->setText(QString::number(latitude, 'f', 6)); m_longitudeEdit->setText(QString::number(longitude, 'f', 6)); // Restore signals m_latitudeEdit->blockSignals(false); m_longitudeEdit->blockSignals(false); qDebug() << "Updated inputs from map:" << latitude << "," << longitude; } void VirtualLocation::onApplyClicked() { m_applyButton->setEnabled(false); bool latOk, lonOk; double latitude = m_latitudeEdit->text().toDouble(&latOk); double longitude = m_longitudeEdit->text().toDouble(&lonOk); if (!latOk || !lonOk) { QMessageBox::warning( this, "Invalid Input", "Please enter valid latitude and longitude values."); m_applyButton->setEnabled(true); return; } DevDiskImageHelper *devDiskImageHelper = new DevDiskImageHelper(m_device, this); connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted, this, [this, latitude, longitude, devDiskImageHelper](bool success) { if (devDiskImageHelper) { devDiskImageHelper->deleteLater(); } if (!success) { return; } // Apply location emit locationChanged(latitude, longitude); updateMapFromInputs(); // Visual feedback m_applyButton->setText("Applied!"); bool locationSuccess = set_location( m_device->device, const_cast( m_latitudeEdit->text().toStdString().c_str()), const_cast( m_longitudeEdit->text().toStdString().c_str())); if (!locationSuccess) { QMessageBox::warning(this, "Error", "Failed to set location on device"); } else { SettingsManager::sharedInstance()->saveRecentLocation( latitude, longitude); QMessageBox::information(this, "Success", "Location applied successfully!"); } }); connect( devDiskImageHelper, &DevDiskImageHelper::destroyed, this, [this]() { QTimer::singleShot(1000, this, [this]() { m_applyButton->setText("Apply Location"); m_applyButton->setEnabled(true); }); }, Qt::SingleShotConnection); devDiskImageHelper->start(); } void VirtualLocation::loadRecentLocations(QVBoxLayout *layout) { QList recentLocations = SettingsManager::sharedInstance()->getRecentLocations(); if (recentLocations.isEmpty()) { return; // Don't render anything if no recent locations } layout->addItem( new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Fixed)); m_recentGroup = new QGroupBox("Recent Locations"); layout->addWidget(m_recentGroup); // A group box needs a layout to contain its children QVBoxLayout *groupBoxLayout = new QVBoxLayout(m_recentGroup); QScrollArea *scrollArea = new QScrollArea(); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); groupBoxLayout->addWidget(scrollArea); QWidget *scrollContent = new QWidget(); scrollArea->setWidget(scrollContent); // This layout is for the content widget QVBoxLayout *recentLayout = new QVBoxLayout(scrollContent); addLocationButtons(recentLayout, recentLocations); } void VirtualLocation::onRecentLocationClicked(double latitude, double longitude) { // Update input fields m_latitudeEdit->setText(QString::number(latitude, 'f', 6)); m_longitudeEdit->setText(QString::number(longitude, 'f', 6)); // Update map updateMapFromInputs(); qDebug() << "Recent location clicked:" << latitude << "," << longitude; } void VirtualLocation::refreshRecentLocations() { if (!m_recentGroup) { return; } // Get the group box's layout QVBoxLayout *groupBoxLayout = qobject_cast(m_recentGroup->layout()); if (!groupBoxLayout) { return; } // Get the scroll area from the group box layout QScrollArea *scrollArea = nullptr; if (groupBoxLayout->count() > 0) { scrollArea = qobject_cast(groupBoxLayout->itemAt(0)->widget()); } if (!scrollArea) { return; } // Get the scroll content widget QWidget *scrollContent = scrollArea->widget(); if (!scrollContent) { return; } // Get the content layout QVBoxLayout *recentLayout = qobject_cast(scrollContent->layout()); if (!recentLayout) { return; } // Clear all existing buttons QLayoutItem *item; while ((item = recentLayout->takeAt(0)) != nullptr) { if (item->widget()) { item->widget()->deleteLater(); } delete item; } // Reload recent locations QList recentLocations = SettingsManager::sharedInstance()->getRecentLocations(); if (recentLocations.isEmpty()) { // Hide the group if no locations m_recentGroup->hide(); return; } // Show the group if it was hidden m_recentGroup->show(); addLocationButtons(recentLayout, recentLocations); } void VirtualLocation::addLocationButtons(QLayout *layout, QList recentLocations) { for (const QVariantMap &location : recentLocations) { double latitude = location["latitude"].toDouble(); double longitude = location["longitude"].toDouble(); QPushButton *locationBtn = new QPushButton(QString("Lat: %1\nLon: %2") .arg(latitude, 0, 'f', 4) .arg(longitude, 0, 'f', 4)); connect(locationBtn, &QPushButton::clicked, this, [this, latitude, longitude]() { onRecentLocationClicked(latitude, longitude); }); layout->addWidget(locationBtn); } }