diff --git a/src/plugins/ios/createsimulatordialog.cpp b/src/plugins/ios/createsimulatordialog.cpp new file mode 100644 index 00000000000..6c8d711b684 --- /dev/null +++ b/src/plugins/ios/createsimulatordialog.cpp @@ -0,0 +1,179 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "createsimulatordialog.h" +#include "ui_createsimulatordialog.h" +#include "simulatorcontrol.h" + +#include +#include + +#include +#include + +namespace Ios { +namespace Internal { + +using namespace std::placeholders; + +CreateSimulatorDialog::CreateSimulatorDialog(QWidget *parent) : + QDialog(parent), + m_ui(new Ui::CreateSimulatorDialog), + m_simControl(new SimulatorControl(this)) +{ + m_ui->setupUi(this); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + const auto enableOk = [this]() { + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled( + !m_ui->nameEdit->text().isEmpty() && + m_ui->deviceTypeCombo->currentIndex() > 0 && + m_ui->runtimeCombo->currentIndex() > 0); + }; + + const auto indexChanged = static_cast(&QComboBox::currentIndexChanged); + connect(m_ui->nameEdit, &QLineEdit::textChanged, enableOk); + connect(m_ui->runtimeCombo, indexChanged, enableOk); + connect(m_ui->deviceTypeCombo, indexChanged, [this, enableOk]() { + populateRuntimes(m_ui->deviceTypeCombo->currentData().value()); + enableOk(); + }); + + m_futureSync.setCancelOnWait(true); + m_futureSync.addFuture(Utils::onResultReady(SimulatorControl::updateDeviceTypes(), this, + &CreateSimulatorDialog::populateDeviceTypes)); + + QFuture> runtimesfuture = SimulatorControl::updateRuntimes(); + Utils::onResultReady(runtimesfuture, this, [this](const QList &runtimes) { + m_runtimes = runtimes; + }); + m_futureSync.addFuture(runtimesfuture); + populateRuntimes(DeviceTypeInfo()); +} + +CreateSimulatorDialog::~CreateSimulatorDialog() +{ + m_futureSync.waitForFinished(); + delete m_ui; +} + +/*! + Returns the the simulator name entered by user. + */ +QString CreateSimulatorDialog::name() const +{ + return m_ui->nameEdit->text(); +} + +/*! + Returns the the simulator runtime (OS version) selected by user. + Though the runtimes are filtered by the selected device type but the runtime camppatibility is + not checked. i.e. User can select the Runtime iOS 10.2 for iPhone 4 but the combination is not + possible as iOS 10.2 is not compatible with iPhone 4. In this case the command to create + simulator shall fail with an error message describing the compatibility. + */ +RuntimeInfo CreateSimulatorDialog::runtime() const +{ + return m_ui->runtimeCombo->currentData().value(); +} + +/*! + Returns the the selected device type. + */ +DeviceTypeInfo CreateSimulatorDialog::deviceType() const +{ + return m_ui->deviceTypeCombo->currentData().value(); +} + +/*! + Populates the devices types. Similar device types are grouped together. + */ +void CreateSimulatorDialog::populateDeviceTypes(const QList &deviceTypes) +{ + m_ui->deviceTypeCombo->clear(); + m_ui->deviceTypeCombo->addItem(tr("None")); + + if (deviceTypes.isEmpty()) + return; + + m_ui->deviceTypeCombo->insertSeparator(1); + + auto addItems = [this, deviceTypes](const QString &filter) { + auto filteredTypes = Utils::filtered(deviceTypes, [filter](const DeviceTypeInfo &type){ + return type.name.contains(filter, Qt::CaseInsensitive); + }); + foreach (auto type, filteredTypes) { + m_ui->deviceTypeCombo->addItem(type.name, QVariant::fromValue(type)); + } + return filteredTypes.count(); + }; + + if (addItems(QStringLiteral("iPhone")) > 0) + m_ui->deviceTypeCombo->insertSeparator(m_ui->deviceTypeCombo->count()); + if (addItems(QStringLiteral("iPad")) > 0) + m_ui->deviceTypeCombo->insertSeparator(m_ui->deviceTypeCombo->count()); + if (addItems(QStringLiteral("TV")) > 0) + m_ui->deviceTypeCombo->insertSeparator(m_ui->deviceTypeCombo->count()); + addItems(QStringLiteral("Watch")); +} + +/*! + Populates the available runtimes. Though the runtimes are filtered by the selected device type + but the runtime camppatibility is not checked. i.e. User can select the Runtime iOS 10.2 for + iPhone 4 but the combination is not possible as iOS 10.2 is not compatible with iPhone 4. In + this case the command to create simulator shall fail with an error message describing the + compatibility issue. + */ +void CreateSimulatorDialog::populateRuntimes(const DeviceTypeInfo &deviceType) +{ + m_ui->runtimeCombo->clear(); + m_ui->runtimeCombo->addItem(tr("None")); + + if (deviceType.name.isEmpty()) + return; + + m_ui->runtimeCombo->insertSeparator(1); + + auto addItems = [this](const QString &filter) { + auto filteredTypes = Utils::filtered(m_runtimes, [filter](const RuntimeInfo &runtime){ + return runtime.name.contains(filter, Qt::CaseInsensitive); + }); + foreach (auto runtime, filteredTypes) { + m_ui->runtimeCombo->addItem(runtime.name, QVariant::fromValue(runtime)); + } + }; + + if (deviceType.name.contains(QStringLiteral("iPhone"))) + addItems(QStringLiteral("iOS")); + else if (deviceType.name.contains(QStringLiteral("iPad"))) + addItems(QStringLiteral("iOS")); + else if (deviceType.name.contains(QStringLiteral("TV"))) + addItems(QStringLiteral("tvOS")); + else if (deviceType.name.contains(QStringLiteral("Watch"))) + addItems(QStringLiteral("watchOS")); +} + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/createsimulatordialog.h b/src/plugins/ios/createsimulatordialog.h new file mode 100644 index 00000000000..f9deb6a3767 --- /dev/null +++ b/src/plugins/ios/createsimulatordialog.h @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +namespace Ios { +namespace Internal { + +namespace Ui { class CreateSimulatorDialog; } +class SimulatorControl; +class RuntimeInfo; +class DeviceTypeInfo; + +/*! + A dialog to select the iOS Device type and the runtime for a new + iOS simulator device. + */ +class CreateSimulatorDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateSimulatorDialog(QWidget *parent = nullptr); + ~CreateSimulatorDialog(); + + QString name() const; + RuntimeInfo runtime() const; + DeviceTypeInfo deviceType() const; + +private: + void populateDeviceTypes(const QList &deviceTypes); + void populateRuntimes(const DeviceTypeInfo &deviceType); + +private: + QFutureSynchronizer m_futureSync; + Ui::CreateSimulatorDialog *m_ui = nullptr; + SimulatorControl *m_simControl = nullptr; + QList m_runtimes; +}; + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/createsimulatordialog.ui b/src/plugins/ios/createsimulatordialog.ui new file mode 100644 index 00000000000..655ff581cf3 --- /dev/null +++ b/src/plugins/ios/createsimulatordialog.ui @@ -0,0 +1,104 @@ + + + Ios::Internal::CreateSimulatorDialog + + + + 0 + 0 + 320 + 160 + + + + + 0 + 0 + + + + Create Simulator + + + + + + + + Simulator name: + + + + + + + + + + Device type: + + + + + + + + + + OS version: + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Ios::Internal::CreateSimulatorDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Ios::Internal::CreateSimulatorDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/plugins/ios/ios.pro b/src/plugins/ios/ios.pro index 07396fbc9dc..13215055815 100644 --- a/src/plugins/ios/ios.pro +++ b/src/plugins/ios/ios.pro @@ -33,7 +33,10 @@ HEADERS += \ iosdeploystepwidget.h \ simulatorcontrol.h \ iosbuildconfiguration.h \ - iosbuildsettingswidget.h + iosbuildsettingswidget.h \ + createsimulatordialog.h \ + simulatoroperationdialog.h \ + simulatorinfomodel.h SOURCES += \ @@ -61,14 +64,19 @@ SOURCES += \ iosdeploystepwidget.cpp \ simulatorcontrol.cpp \ iosbuildconfiguration.cpp \ - iosbuildsettingswidget.cpp + iosbuildsettingswidget.cpp \ + createsimulatordialog.cpp \ + simulatoroperationdialog.cpp \ + simulatorinfomodel.cpp FORMS += \ iossettingswidget.ui \ iosbuildstep.ui \ iosdeploystepwidget.ui \ iospresetbuildstep.ui \ - iosbuildsettingswidget.ui + iosbuildsettingswidget.ui \ + createsimulatordialog.ui \ + simulatoroperationdialog.ui DEFINES += IOS_LIBRARY diff --git a/src/plugins/ios/ios.qbs b/src/plugins/ios/ios.qbs index 0e67c9a8579..398c3f43e69 100644 --- a/src/plugins/ios/ios.qbs +++ b/src/plugins/ios/ios.qbs @@ -14,6 +14,9 @@ QtcPlugin { cpp.frameworks: base.concat(qbs.targetOS.contains("macos") ? ["CoreFoundation", "IOKit"] : []) files: [ + "createsimulatordialog.cpp", + "createsimulatordialog.h", + "createsimulatordialog.ui", "ios.qrc", "iosbuildconfiguration.cpp", "iosbuildconfiguration.h", @@ -70,6 +73,11 @@ QtcPlugin { "iostoolhandler.cpp", "iostoolhandler.h", "simulatorcontrol.cpp", - "simulatorcontrol.h" + "simulatorcontrol.h", + "simulatorinfomodel.cpp", + "simulatorinfomodel.h", + "simulatoroperationdialog.cpp", + "simulatoroperationdialog.h", + "simulatoroperationdialog.ui" ] } diff --git a/src/plugins/ios/iosconfigurations.cpp b/src/plugins/ios/iosconfigurations.cpp index e3ebe913d81..6c8ae3b37f4 100644 --- a/src/plugins/ios/iosconfigurations.cpp +++ b/src/plugins/ios/iosconfigurations.cpp @@ -59,6 +59,8 @@ #include #include #include +#include +#include #include using namespace ProjectExplorer; @@ -77,6 +79,7 @@ namespace Internal { const QLatin1String SettingsGroup("IosConfigurations"); const QLatin1String ignoreAllDevicesKey("IgnoreAllDevices"); +const char screenshotDirPathKey[] = "ScreeshotDirPath"; const char provisioningTeamsTag[] = "IDEProvisioningTeams"; const char freeTeamTag[] = "isFreeProvisioningTeam"; @@ -355,6 +358,19 @@ void IosConfigurations::setIgnoreAllDevices(bool ignoreDevices) } } +void IosConfigurations::setScreenshotDir(const FileName &path) +{ + if (m_instance->m_screenshotDir != path) { + m_instance->m_screenshotDir = path; + m_instance->save(); + } +} + +FileName IosConfigurations::screenshotDir() +{ + return m_instance->m_screenshotDir; +} + FileName IosConfigurations::developerPath() { return m_instance->m_developerPath; @@ -370,6 +386,7 @@ void IosConfigurations::save() QSettings *settings = Core::ICore::settings(); settings->beginGroup(SettingsGroup); settings->setValue(ignoreAllDevicesKey, m_ignoreAllDevices); + settings->setValue(screenshotDirPathKey, m_screenshotDir.toString()); settings->endGroup(); } @@ -384,6 +401,12 @@ void IosConfigurations::load() QSettings *settings = Core::ICore::settings(); settings->beginGroup(SettingsGroup); m_ignoreAllDevices = settings->value(ignoreAllDevicesKey, false).toBool(); + m_screenshotDir = FileName::fromString(settings->value(screenshotDirPathKey).toString()); + if (!m_screenshotDir.exists()) { + QString defaultDir = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first(); + m_screenshotDir = FileName::fromString(defaultDir); + } + settings->endGroup(); } diff --git a/src/plugins/ios/iosconfigurations.h b/src/plugins/ios/iosconfigurations.h index 55daf3a1a12..8b00048cf3b 100644 --- a/src/plugins/ios/iosconfigurations.h +++ b/src/plugins/ios/iosconfigurations.h @@ -113,6 +113,8 @@ public: static void initialize(); static bool ignoreAllDevices(); static void setIgnoreAllDevices(bool ignoreDevices); + static void setScreenshotDir(const Utils::FileName &path); + static Utils::FileName screenshotDir(); static Utils::FileName developerPath(); static QVersionNumber xcodeVersion(); static Utils::FileName lldbPath(); @@ -135,6 +137,7 @@ private: void loadProvisioningData(bool notify = true); Utils::FileName m_developerPath; + Utils::FileName m_screenshotDir; QVersionNumber m_xcodeVersion; bool m_ignoreAllDevices; QFileSystemWatcher *m_provisioningDataWatcher = nullptr; diff --git a/src/plugins/ios/iossettingswidget.cpp b/src/plugins/ios/iossettingswidget.cpp index 2d19582d8a3..4754ae8b97d 100644 --- a/src/plugins/ios/iossettingswidget.cpp +++ b/src/plugins/ios/iossettingswidget.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. @@ -24,36 +24,68 @@ ****************************************************************************/ #include "iossettingswidget.h" - #include "ui_iossettingswidget.h" +#include "createsimulatordialog.h" #include "iosconfigurations.h" -#include "iosconstants.h" +#include "simulatorinfomodel.h" +#include "simulatoroperationdialog.h" -#include -#include -#include -#include -#include -#include +#include +#include -#include -#include -#include - -#include +#include +#include #include -#include +#include + +static const int simStartWarnCount = 4; namespace Ios { namespace Internal { +using namespace std::placeholders; + +static SimulatorInfoList selectedSimulators(const QTreeView *deviceTreeView) +{ + SimulatorInfoList list; + QItemSelectionModel *selectionModel = deviceTreeView->selectionModel(); + for (QModelIndex index: selectionModel->selectedRows()) + list << deviceTreeView->model()->data(index, Qt::UserRole).value(); + return list; +} + +static void onSimOperation(const SimulatorInfo &simInfo, SimulatorOperationDialog* dlg, + const QString &contextStr, const SimulatorControl::ResponseData &response) +{ + dlg->addMessage(simInfo, response, contextStr); +} + IosSettingsWidget::IosSettingsWidget(QWidget *parent) : QWidget(parent), m_ui(new Ui::IosSettingsWidget), - m_saveSettingsRequested(false) + m_simControl(new SimulatorControl(this)), + m_simInfoModel( new SimulatorInfoModel(this)) { - initGui(); + m_ui->setupUi(this); + m_ui->deviceView->setModel(m_simInfoModel); + m_ui->deviceView->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_ui->pathWidget->setExpectedKind(Utils::PathChooser::ExistingDirectory); + m_ui->pathWidget->lineEdit()->setReadOnly(true); + m_ui->pathWidget->setFileName(IosConfigurations::screenshotDir()); + m_ui->pathWidget->addButton(tr("Screenshot"), this, + std::bind(&IosSettingsWidget::onScreenshot, this)); + + m_ui->deviceAskCheckBox->setChecked(!IosConfigurations::ignoreAllDevices()); + + connect(m_ui->startButton, &QPushButton::clicked, this, &IosSettingsWidget::onStart); + connect(m_ui->createButton, &QPushButton::clicked, this, &IosSettingsWidget::onCreate); + connect(m_ui->renameButton, &QPushButton::clicked, this, &IosSettingsWidget::onRename); + connect(m_ui->resetButton, &QPushButton::clicked, this, &IosSettingsWidget::onReset); + connect(m_ui->deleteButton, &QPushButton::clicked, this, &IosSettingsWidget::onDelete); + + connect(m_ui->deviceView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + &IosSettingsWidget::onSelectionChanged); } IosSettingsWidget::~IosSettingsWidget() @@ -61,16 +93,225 @@ IosSettingsWidget::~IosSettingsWidget() delete m_ui; } -void IosSettingsWidget::initGui() +/*! + Called on start button click. Selected simulator devices are started. Multiple devices can be + started simultaneously provided they in shutdown state. + */ +void IosSettingsWidget::onStart() { - m_ui->setupUi(this); - m_ui->deviceAskCheckBox->setChecked(!IosConfigurations::ignoreAllDevices()); + const SimulatorInfoList simulatorInfoList = selectedSimulators(m_ui->deviceView); + if (simulatorInfoList.isEmpty()) + return; + + if (simulatorInfoList.count() > simStartWarnCount) { + const QString message = tr("You are trying to launch %n simulators simultaneously. This " + "will take significant system resources. Do you really want to " + "continue?", "", simulatorInfoList.count()); + const int buttonCode = QMessageBox::warning(this, tr("Simulator Start"), message, + QMessageBox::Ok | QMessageBox::Abort, + QMessageBox::Abort); + + if (buttonCode == QMessageBox::Abort) + return; + } + + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Starting simulator devices...", "", simulatorInfoList.count()), + Utils::NormalMessageFormat); + + QList> futureList; + foreach (const SimulatorInfo &info, simulatorInfoList) { + if (!info.isShutdown()) { + statusDialog->addMessage(tr("Cannot start simulator(%1, %2) in current state: %3") + .arg(info.name).arg(info.runtimeName).arg(info.state), + Utils::StdErrFormat); + } else { + futureList << Utils::onResultReady(m_simControl->startSimulator(info.identifier), + std::bind(onSimOperation, info, statusDialog, + tr("simulator start"), _1)); + } + } + + statusDialog->addFutures(futureList); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. +} + +/*! + Called on create button click. User is presented with the create simulator dialog and with the + selected options a new device is created. + */ +void IosSettingsWidget::onCreate() +{ + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Creating simulator device..."), Utils::NormalMessageFormat); + const auto onSimulatorCreate = [this, statusDialog](const QString &name, + const SimulatorControl::ResponseData &response) { + if (response.success) { + statusDialog->addMessage(tr("Simulator device(%1) created.\nUDID: %2") + .arg(name).arg(response.simUdid), Utils::StdOutFormat); + } else { + statusDialog->addMessage(tr("Simulator device(%1) creation failed.\nError: %2"). + arg(name).arg(QString::fromUtf8(response.commandOutput)), + Utils::StdErrFormat); + } + }; + + CreateSimulatorDialog createDialog(this); + if (createDialog.exec() == QDialog::Accepted) { + QFuture f = Utils::onResultReady( + m_simControl->createSimulator( + createDialog.name(), + createDialog.deviceType(), + createDialog.runtime()), + std::bind(onSimulatorCreate, createDialog.name(), _1)); + statusDialog->addFutures({ f }); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. + } +} + +/*! + Called on reset button click. Contents and settings of the selected devices are erased. Multiple + devices can be erased simultaneously provided they in shutdown state. + */ +void IosSettingsWidget::onReset() +{ + const SimulatorInfoList simulatorInfoList = selectedSimulators(m_ui->deviceView); + if (simulatorInfoList.isEmpty()) + return; + + const int userInput = QMessageBox::question(this, tr("Reset"), + tr("Do you really want to reset the contents and settings" + " of the selected devices", "", + simulatorInfoList.count())); + if (userInput == QMessageBox::No) + return; + + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Resetting contents and settings..."), Utils::NormalMessageFormat); + + QList> futureList; + foreach (const SimulatorInfo &info, simulatorInfoList) { + futureList << Utils::onResultReady(m_simControl->resetSimulator(info.identifier), + std::bind(onSimOperation, info, statusDialog, + tr("simulator reset"), _1)); + } + + statusDialog->addFutures(futureList); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. +} + +/*! + Called on rename button click. Selected device is renamed. Only one device can be renamed at a + time. Rename button is disabled on multi-selection. + */ +void IosSettingsWidget::onRename() +{ + const SimulatorInfoList simulatorInfoList = selectedSimulators(m_ui->deviceView); + if (simulatorInfoList.isEmpty() || simulatorInfoList.count() > 1) + return; + + const SimulatorInfo &simInfo = simulatorInfoList.at(0); + const QString newName = QInputDialog::getText(this, tr("Rename %1").arg(simInfo.name), + tr("Enter new name:")); + + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Renaming simulator device..."), Utils::NormalMessageFormat); + QFuture f = Utils::onResultReady(m_simControl->renameSimulator(simInfo.identifier, newName), + std::bind(onSimOperation, simInfo, statusDialog, + tr("simulator rename"), _1)); + statusDialog->addFutures({f}); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. +} + +/*! + Called on delete button click. Selected devices are deleted. Multiple devices can be deleted + simultaneously provided they in shutdown state. + */ +void IosSettingsWidget::onDelete() +{ + const SimulatorInfoList simulatorInfoList = selectedSimulators(m_ui->deviceView); + if (simulatorInfoList.isEmpty()) + return; + + const int userInput = QMessageBox::question(this, tr("Delete Device"), + tr("Do you really want to delete the selected " + "devices", "", simulatorInfoList.count())); + if (userInput == QMessageBox::No) + return; + + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Deleting simulator devices...", "", simulatorInfoList.count()), + Utils::NormalMessageFormat); + QList> futureList; + foreach (const SimulatorInfo &info, simulatorInfoList) { + futureList << Utils::onResultReady(m_simControl->deleteSimulator(info.identifier), + std::bind(onSimOperation, info, statusDialog, + tr("simulator delete"), _1)); + } + + statusDialog->addFutures(futureList); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. +} + +/*! + Called on screenshot button click. Screenshot of the selected devices are saved to the selected + path. Screenshot from multiple devices can be taken simultaneously provided they in booted state. + */ +void IosSettingsWidget::onScreenshot() +{ + const SimulatorInfoList simulatorInfoList = selectedSimulators(m_ui->deviceView); + if (simulatorInfoList.isEmpty()) + return; + + const auto generatePath = [this](const SimulatorInfo &info) { + const QString fileName = QString("%1_%2_%3.png").arg(info.name).arg(info.runtimeName) + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss-z")).replace(' ', '_'); + return m_ui->pathWidget->fileName().appendPath(fileName).toString(); + }; + + QPointer statusDialog = new SimulatorOperationDialog(this); + statusDialog->setAttribute(Qt::WA_DeleteOnClose); + statusDialog->addMessage(tr("Capturing screenshots from devices...", "", + simulatorInfoList.count()), Utils::NormalMessageFormat); + QList> futureList; + foreach (const SimulatorInfo &info, simulatorInfoList) { + futureList << Utils::onResultReady(m_simControl->takeSceenshot(info.identifier, + generatePath(info)), + std::bind(onSimOperation, info, statusDialog, + tr("simulator screenshot"), _1)); + } + + statusDialog->addFutures(futureList); + statusDialog->exec(); // Modal dialog returns only when all the operations are done or cancelled. +} + +void IosSettingsWidget::onSelectionChanged() +{ + const SimulatorInfoList infoList = selectedSimulators(m_ui->deviceView); + const bool hasRunning = Utils::anyOf(infoList, [](const SimulatorInfo &info) { + return info.isBooted(); + }); + const bool hasShutdown = Utils::anyOf(infoList, [](const SimulatorInfo &info) { + return info.isShutdown(); + }); + m_ui->startButton->setEnabled(hasShutdown); + m_ui->deleteButton->setEnabled(hasShutdown); + m_ui->resetButton->setEnabled(hasShutdown); + m_ui->renameButton->setEnabled(infoList.count() == 1 && hasShutdown); + m_ui->pathWidget->buttonAtIndex(1)->setEnabled(hasRunning); // Screenshot button } void IosSettingsWidget::saveSettings() { IosConfigurations::setIgnoreAllDevices(!m_ui->deviceAskCheckBox->isChecked()); + IosConfigurations::setScreenshotDir(m_ui->pathWidget->fileName()); } } // namespace Internal } // namespace Ios + diff --git a/src/plugins/ios/iossettingswidget.h b/src/plugins/ios/iossettingswidget.h index 2fa828489be..65baa384398 100644 --- a/src/plugins/ios/iossettingswidget.h +++ b/src/plugins/ios/iossettingswidget.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. @@ -26,32 +26,40 @@ #pragma once #include "iosconfigurations.h" - -#include -#include +#include "simulatorcontrol.h" #include -#include namespace Ios { namespace Internal { namespace Ui { class IosSettingsWidget; } +class SimulatorInfoModel; +using SimulatorInfoList = QList; class IosSettingsWidget : public QWidget { Q_OBJECT + public: - // Todo: This would be so much simpler if it just used Utils::PathChooser!!! IosSettingsWidget(QWidget *parent = 0); ~IosSettingsWidget(); void saveSettings(); private: - void initGui(); + void onStart(); + void onCreate(); + void onReset(); + void onRename(); + void onDelete(); + void onScreenshot(); + void onSelectionChanged(); - Ui::IosSettingsWidget *m_ui; - bool m_saveSettingsRequested; +private: + Ui::IosSettingsWidget *m_ui = nullptr; + bool m_saveSettingsRequested = false; + SimulatorControl *m_simControl = nullptr; + SimulatorInfoModel *m_simInfoModel = nullptr; }; } // namespace Internal diff --git a/src/plugins/ios/iossettingswidget.ui b/src/plugins/ios/iossettingswidget.ui index f1ae1525ac8..e27b4b31637 100644 --- a/src/plugins/ios/iossettingswidget.ui +++ b/src/plugins/ios/iossettingswidget.ui @@ -1,54 +1,210 @@ Ios::Internal::IosSettingsWidget - + 0 0 - 679 - 184 + 622 + 456 iOS Configuration - + - - - - - - 0 - 0 - - - - Ask about devices not in developer mode - - - true - - - - + + + Devices + + + + + + + 0 + 0 + + + + Ask about devices not in developer mode + + + true + + + + + - - - Qt::Vertical + + + Simulator - - - 20 - 40 - - - + + + + + Rename a simulator device. + + + Rename + + + + + + + + 0 + 0 + + + + Delete simulator devices. + + + Delete + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + Reset contents and settings of simulator devices. + + + Reset + + + + + + + + 0 + 0 + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + Screenshot directory: + + + + + + + + 0 + 0 + + + + + + + + + + QFrame::Raised + + + Qt::Horizontal + + + + + + + Create a new simulator device. + + + Create + + + + + + + + 0 + 0 + + + + Start simulator devices. + + + Start + + + + + + + + Utils::PathChooser + QWidget +
utils/pathchooser.h
+ 1 +
+
diff --git a/src/plugins/ios/simulatorcontrol.cpp b/src/plugins/ios/simulatorcontrol.cpp index 84b5b094de8..9b652dae849 100644 --- a/src/plugins/ios/simulatorcontrol.cpp +++ b/src/plugins/ios/simulatorcontrol.cpp @@ -574,5 +574,14 @@ QDebug &operator<<(QDebug &stream, const SimulatorInfo &info) return stream; } +bool SimulatorInfo::operator==(const SimulatorInfo &other) const +{ + return identifier == other.identifier + && state == other.state + && name == other.name + && available == other.available + && runtimeName == other.runtimeName; +} + } // namespace Internal } // namespace Ios diff --git a/src/plugins/ios/simulatorcontrol.h b/src/plugins/ios/simulatorcontrol.h index 5c95d00f220..5c01eee5ecc 100644 --- a/src/plugins/ios/simulatorcontrol.h +++ b/src/plugins/ios/simulatorcontrol.h @@ -58,6 +58,8 @@ public: bool isBooted() const { return state.compare(QStringLiteral("Booted")) == 0; } bool isShutdown() const { return state.compare(QStringLiteral("Shutdown")) == 0; } bool isShuttingDown() const { return state == "Shutting Down"; } + bool operator==(const SimulatorInfo &other) const; + bool operator!=(const SimulatorInfo &other) const { return !(*this == other); } bool available; QString state; QString runtimeName; diff --git a/src/plugins/ios/simulatorinfomodel.cpp b/src/plugins/ios/simulatorinfomodel.cpp new file mode 100644 index 00000000000..f5576da6c9c --- /dev/null +++ b/src/plugins/ios/simulatorinfomodel.cpp @@ -0,0 +1,166 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "simulatorinfomodel.h" + +#include +#include + +#include + +namespace Ios { +namespace Internal { + +using namespace std::placeholders; + +const int colCount = 3; +const int nameCol = 0; +const int runtimeCol = 1; +const int stateCol = 2; +static const int deviceUpdateInterval = 1000; // Update simulator state every 1 sec. + +SimulatorInfoModel::SimulatorInfoModel(QObject *parent) : + QAbstractItemModel(parent) +{ + m_fetchFuture.setCancelOnWait(true); + + requestSimulatorInfo(); + + auto updateTimer = new QTimer(this); + connect(updateTimer, &QTimer::timeout, this, &SimulatorInfoModel::requestSimulatorInfo); + updateTimer->setInterval(deviceUpdateInterval); + updateTimer->start(); +} + +QVariant SimulatorInfoModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + const SimulatorInfo &simInfo = m_simList[index.row()]; + if (role == Qt::EditRole || role == Qt::DisplayRole) { + switch (index.column()) { + case nameCol: + return simInfo.name; + case runtimeCol: + return simInfo.runtimeName; + case stateCol: + return simInfo.state; + default: + return ""; + } + } else if (role == Qt::ToolTipRole) { + return tr("UDID: %1").arg(simInfo.identifier); + } else if (role == Qt::UserRole) { + return QVariant::fromValue(simInfo); + } + + return QVariant(); +} + +int SimulatorInfoModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return m_simList.count(); + return 0; +} + +int SimulatorInfoModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return colCount; +} + +QVariant SimulatorInfoModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical || section > colCount) + return QVariant(); + + if (role == Qt::DisplayRole) { + switch (section) { + case nameCol: + return tr("Simulator Name"); + case runtimeCol: + return tr("Runtime"); + case stateCol: + return tr("Current State"); + default: + return ""; + } + } + + return QVariant(); +} + +QModelIndex SimulatorInfoModel::index(int row, int column, const QModelIndex &parent) const +{ + return hasIndex(row, column, parent) ? createIndex(row, column) : QModelIndex(); +} + +QModelIndex SimulatorInfoModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +void SimulatorInfoModel::requestSimulatorInfo() +{ + if (!m_fetchFuture.futures().isEmpty() && !m_fetchFuture.futures().at(0).isFinished()) + return; // Ignore the request if the last request is still pending. + + m_fetchFuture.clearFutures(); + m_fetchFuture.addFuture(Utils::onResultReady(SimulatorControl::updateAvailableSimulators(), + this, &SimulatorInfoModel::populateSimulators)); +} + +void SimulatorInfoModel::populateSimulators(const SimulatorInfoList &simulatorList) +{ + if (m_simList.isEmpty() || m_simList.count() != simulatorList.count()) { + // Reset the model in case of addition or deletion. + beginResetModel(); + m_simList = simulatorList; + endResetModel(); + } else { + // update the rows with data chagne. e.g. state changes. + auto newItr = simulatorList.cbegin(); + int start = -1, end = -1; + std::list> updatedIndexes; + for (auto itr = m_simList.cbegin(); itr < m_simList.cend(); ++itr, ++newItr) { + if (*itr == *newItr) { + if (end != -1) + updatedIndexes.push_back(std::make_pair(start, end - 1)); + start = std::distance(m_simList.cbegin(), itr); + end = -1; + } else { + end = std::distance(m_simList.cbegin(), itr); + } + } + m_simList = simulatorList; + for (auto pair: updatedIndexes) + emit dataChanged(index(pair.first,0), index(pair.second, colCount - 1)); + } +} + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/simulatorinfomodel.h b/src/plugins/ios/simulatorinfomodel.h new file mode 100644 index 00000000000..174865e7b56 --- /dev/null +++ b/src/plugins/ios/simulatorinfomodel.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "simulatorcontrol.h" + +#include +#include + +namespace Ios { +namespace Internal { + +using SimulatorInfoList = QList; + +class SimulatorInfoModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + SimulatorInfoModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &) const override; + +private: + void requestSimulatorInfo(); + void populateSimulators(const SimulatorInfoList &simulatorList); + +private: + QFutureSynchronizer m_fetchFuture; + SimulatorInfoList m_simList; +}; + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/simulatoroperationdialog.cpp b/src/plugins/ios/simulatoroperationdialog.cpp new file mode 100644 index 00000000000..6e687d8437f --- /dev/null +++ b/src/plugins/ios/simulatoroperationdialog.cpp @@ -0,0 +1,130 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "simulatoroperationdialog.h" +#include "ui_simulatoroperationdialog.h" + +#include +#include + +#include +#include +#include + +namespace { +Q_LOGGING_CATEGORY(iosCommon, "qtc.ios.common") +} + +namespace Ios { +namespace Internal { + +SimulatorOperationDialog::SimulatorOperationDialog(QWidget *parent) : + // TODO: Maximize buttong only because of QTBUG-41932 + QDialog(parent,Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMaximizeButtonHint), + m_ui(new Ui::SimulatorOperationDialog) +{ + m_ui->setupUi(this); + + m_formatter = new Utils::OutputFormatter; + m_formatter->setPlainTextEdit(m_ui->messageEdit); +} + +SimulatorOperationDialog::~SimulatorOperationDialog() +{ + // Cancel all pending futures. + foreach (auto watcher, m_futureWatchList) { + if (!watcher->isFinished()) + watcher->cancel(); + } + + // wait for futures to finish + foreach (auto watcher, m_futureWatchList) { + if (!watcher->isFinished()) + watcher->waitForFinished(); + delete watcher; + } + + delete m_formatter; + delete m_ui; +} + +void SimulatorOperationDialog::addFutures(const QList > &futureList) +{ + foreach (auto future, futureList) { + if (!future.isFinished() || !future.isCanceled()) { + auto watcher = new QFutureWatcher; + watcher->setFuture(future); + connect(watcher, &QFutureWatcher::finished, + this, &SimulatorOperationDialog::futureFinished); + m_futureWatchList << watcher; + } + } + updateInputs(); +} + +void SimulatorOperationDialog::addMessage(const QString &message, Utils::OutputFormat format) +{ + m_formatter->appendMessage(message + "\n\n", format); +} + +void SimulatorOperationDialog::addMessage(const SimulatorInfo &siminfo, + const SimulatorControl::ResponseData &response, + const QString &context) +{ + QTC_CHECK(siminfo.identifier == response.simUdid); + if (response.success) { + addMessage(tr("%1, %2\nOperation %3 completed successfully.").arg(siminfo.name) + .arg(siminfo.runtimeName).arg(context), Utils::StdOutFormat); + } else { + QByteArray erroMsg = response.commandOutput.trimmed(); + QString message = tr("%1, %2\nOperation %3 failed.\nUDID: %4\nError: %5").arg(siminfo.name) + .arg(siminfo.runtimeName).arg(context).arg(siminfo.identifier) + .arg(erroMsg.isEmpty() ? tr("Unknown") : QString::fromUtf8(erroMsg)); + addMessage(message, Utils::StdErrFormat); + qCDebug(iosCommon) << message; + } +} + +void SimulatorOperationDialog::futureFinished() +{ + auto watcher = static_cast *>(sender()); + m_futureWatchList.removeAll(watcher); + watcher->deleteLater(); + updateInputs(); +} + +void SimulatorOperationDialog::updateInputs() +{ + bool enableOk = m_futureWatchList.isEmpty(); + m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(!enableOk); + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enableOk); + if (enableOk) { + addMessage(tr("Done."), Utils::NormalMessageFormat); + m_ui->progressBar->setMaximum(1); // Stop progress bar. + } +} + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/simulatoroperationdialog.h b/src/plugins/ios/simulatoroperationdialog.h new file mode 100644 index 00000000000..5afa123f73c --- /dev/null +++ b/src/plugins/ios/simulatoroperationdialog.h @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "simulatorcontrol.h" + +#include + +#include +#include +#include + +namespace Utils { class OutputFormatter; } + +namespace Ios { +namespace Internal { + +namespace Ui { class SimulatorOperationDialog; } + +class SimulatorOperationDialog : public QDialog +{ + Q_OBJECT +public: + explicit SimulatorOperationDialog(QWidget *parent = nullptr); + ~SimulatorOperationDialog(); + +public: + void addFutures(const QList > &futureList); + void addMessage(const QString &message, Utils::OutputFormat format); + void addMessage(const SimulatorInfo &siminfo, const SimulatorControl::ResponseData &response, + const QString &context); + +private: + void futureFinished(); + void updateInputs(); + +private: + Ui::SimulatorOperationDialog *m_ui = nullptr; + Utils::OutputFormatter *m_formatter = nullptr; + QList *> m_futureWatchList; +}; + +} // namespace Internal +} // namespace Ios diff --git a/src/plugins/ios/simulatoroperationdialog.ui b/src/plugins/ios/simulatoroperationdialog.ui new file mode 100644 index 00000000000..863ed761450 --- /dev/null +++ b/src/plugins/ios/simulatoroperationdialog.ui @@ -0,0 +1,87 @@ + + + Ios::Internal::SimulatorOperationDialog + + + + 0 + 0 + 580 + 320 + + + + Simulator Operation Status + + + true + + + + + + true + + + + + + + 0 + + + -1 + + + + + + + true + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Ios::Internal::SimulatorOperationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Ios::Internal::SimulatorOperationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +