mirror of
https://github.com/iDescriptor/iDescriptor.git
synced 2026-06-21 19:35:49 +08:00
483 lines
16 KiB
C++
483 lines
16 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 "virtuallocationwidget.h"
|
|
|
|
// FIXME: on macOS setupToolFrame in Tool widget does nothing
|
|
// probably because we are using a QQuickWidget
|
|
VirtualLocation::VirtualLocation(
|
|
const std::shared_ptr<iDescriptorDevice> device, QWidget *parent)
|
|
: QWidget(parent), m_device(device)
|
|
{
|
|
setWindowTitle("Virtual Location - iDescriptor");
|
|
setMinimumSize(600, 400);
|
|
resize(800, 600);
|
|
|
|
#ifdef WIN32
|
|
setObjectName("VirtualLocationWidget");
|
|
setAttribute(Qt::WA_StyledBackground, true);
|
|
#endif
|
|
|
|
// 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);
|
|
|
|
connect(AppContext::sharedInstance(), &AppContext::deviceRemoved, this,
|
|
[this](const QString &udid) {
|
|
if (m_device->udid == udid) {
|
|
this->close();
|
|
this->deleteLater();
|
|
}
|
|
});
|
|
}
|
|
|
|
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
|
|
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);
|
|
|
|
// FIXME: warn if not valid
|
|
if (latOk && lonOk && latitude >= -90 && latitude <= 90 &&
|
|
longitude >= -180 && longitude <= 180) {
|
|
QQuickItem *rootObject = m_quickWidget->rootObject();
|
|
if (rootObject) {
|
|
QQuickItem *mapItem = rootObject->findChild<QQuickItem *>("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<QQuickItem *>("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<QGeoCoordinate>();
|
|
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// called from QML
|
|
void VirtualLocation::updateInputsFromMap(const QString &latitude,
|
|
const QString &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(latitude);
|
|
m_longitudeEdit->setText(longitude);
|
|
|
|
// Restore signals
|
|
m_latitudeEdit->blockSignals(false);
|
|
m_longitudeEdit->blockSignals(false);
|
|
|
|
qDebug() << "Updated inputs from map:" << latitude << "," << longitude;
|
|
}
|
|
|
|
void VirtualLocation::restoreButtons()
|
|
{
|
|
QTimer::singleShot(300, this, [this]() {
|
|
m_applyButton->setText("Apply Location");
|
|
m_applyButton->setEnabled(true);
|
|
});
|
|
}
|
|
|
|
void VirtualLocation::onApplyClicked()
|
|
{
|
|
m_applyButton->setEnabled(false);
|
|
QString latitude = m_latitudeEdit->text();
|
|
QString longitude = m_longitudeEdit->text();
|
|
|
|
if (longitude.isEmpty() || latitude.isEmpty()) {
|
|
QMessageBox::warning(
|
|
this, "Invalid Input",
|
|
"Please enter valid latitude and longitude values.");
|
|
m_applyButton->setEnabled(true);
|
|
return;
|
|
}
|
|
|
|
int major = m_device->deviceInfo.parsedDeviceVersion.major;
|
|
|
|
qDebug() << "Setting location to:" << latitude << "," << longitude;
|
|
|
|
if (major < 17) {
|
|
DevDiskImageHelper *devDiskImageHelper =
|
|
new DevDiskImageHelper(m_device, this);
|
|
connect(devDiskImageHelper, &DevDiskImageHelper::mountingCompleted,
|
|
this,
|
|
[this, latitude, longitude, devDiskImageHelper](bool success) {
|
|
if (!success) {
|
|
// mounter will show its own error message
|
|
return;
|
|
}
|
|
|
|
u_int32_t set_location_success =
|
|
m_device->service_manager->set_location(latitude,
|
|
longitude);
|
|
|
|
if (set_location_success != 0) {
|
|
qDebug() << "Failed to set location simulation";
|
|
QMessageBox::warning(
|
|
this, "Error",
|
|
QString("Failed to set location simulation"));
|
|
|
|
} else {
|
|
|
|
QMessageBox::information(
|
|
this, "Success", "Location applied successfully!");
|
|
|
|
SettingsManager::sharedInstance()->saveRecentLocation(
|
|
latitude, longitude);
|
|
}
|
|
});
|
|
connect(devDiskImageHelper, &DevDiskImageHelper::destroyed, this,
|
|
&VirtualLocation::restoreButtons, Qt::SingleShotConnection);
|
|
return devDiskImageHelper->start();
|
|
}
|
|
|
|
/* iOS 17 and above */
|
|
int32_t set_location_res =
|
|
m_device->service_manager->set_location(latitude, longitude);
|
|
|
|
switch (set_location_res) {
|
|
case 0:
|
|
QMessageBox::information(this, "Success",
|
|
"Location applied successfully");
|
|
break;
|
|
case ServiceNotFoundErrorCode:
|
|
DevModeWidget(m_device, this).exec();
|
|
break;
|
|
case TimeoutErrorCode:
|
|
qDebug() << "Failed to set location simulation: timed out";
|
|
QMessageBox::warning(this, "Error",
|
|
"Failed to set location simulation: timed out");
|
|
break;
|
|
default:
|
|
qDebug() << "Failed to set location simulation: device not found";
|
|
QMessageBox::warning(this, "Error",
|
|
"Failed to set location simulation: Error code: " +
|
|
QString::number(set_location_res));
|
|
}
|
|
|
|
restoreButtons();
|
|
}
|
|
|
|
void VirtualLocation::loadRecentLocations(QVBoxLayout *layout)
|
|
{
|
|
QList<QVariantMap> 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(const QString &latitude,
|
|
const QString &longitude)
|
|
{
|
|
// Update input fields
|
|
m_latitudeEdit->setText(latitude);
|
|
m_longitudeEdit->setText(longitude);
|
|
// 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<QVBoxLayout *>(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<QScrollArea *>(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<QVBoxLayout *>(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<QVariantMap> 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<QVariantMap> recentLocations)
|
|
{
|
|
for (const QVariantMap &location : recentLocations) {
|
|
QString latitude = location["latitude"].toString();
|
|
QString longitude = location["longitude"].toString();
|
|
|
|
QPushButton *locationBtn = new QPushButton(
|
|
QString("Lat: %1\nLon: %2").arg(latitude).arg(longitude));
|
|
|
|
connect(locationBtn, &QPushButton::clicked, this,
|
|
[this, latitude, longitude]() {
|
|
onRecentLocationClicked(latitude, longitude);
|
|
});
|
|
|
|
layout->addWidget(locationBtn);
|
|
}
|
|
}
|