| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // This software is licensed under the terms of the GNU General Public |
| // License version 2, as published by the Free Software Foundation, and |
| // may be copied, distributed, and modified under those terms. |
| // |
| // 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 General Public License for more details. |
| |
| #include "android/skin/qt/extended-pages/record-macro-page.h" |
| |
| #include "android/base/StringFormat.h" |
| #include "android/base/files/PathUtils.h" |
| #include "android/base/system/System.h" |
| #include "android/emulation/control/automation_agent.h" |
| #include "android/featurecontrol/FeatureControl.h" |
| #include "android/skin/qt/extended-pages/common.h" |
| #include "android/skin/qt/extended-pages/record-macro-edit-dialog.h" |
| #include "android/skin/qt/qt-settings.h" |
| #include "android/skin/qt/stylesheet.h" |
| #include "android/skin/qt/video-player/QtVideoPlayerNotifier.h" |
| |
| #include <QDateTime> |
| #include <QDialogButtonBox> |
| #include <QDir> |
| #include <QLineEdit> |
| #include <QMessageBox> |
| #include <QRegExp> |
| #include <QStandardPaths> |
| #include <QVBoxLayout> |
| #include <iostream> |
| #include <sstream> |
| |
| static const char SECONDS_RECORDING[] = "%1s Recording..."; |
| |
| using android::metrics::MetricsReporter; |
| using android_studio::EmulatorAutomation; |
| using EmulatorAutomationPresetMacro = |
| android_studio::EmulatorAutomation::EmulatorAutomationPresetMacro; |
| |
| using namespace android::base; |
| |
| // Allow at most 5 reports every 60 seconds. |
| static constexpr uint64_t kReportWindowDurationUs = 1000 * 1000 * 60; |
| static constexpr uint32_t kMaxReportsPerWindow = 5; |
| |
| const QAndroidAutomationAgent* RecordMacroPage::sAutomationAgent = nullptr; |
| RecordMacroPage* RecordMacroPage::sInstance = nullptr; |
| |
| RecordMacroPage::RecordMacroPage(QWidget* parent) |
| : QWidget(parent), mUi(new Ui::RecordMacroPage()) { |
| mUi->setupUi(this); |
| sInstance = this; |
| |
| mRecordEnabled = android::featurecontrol::isEnabled( |
| android::featurecontrol::MacroUi); |
| if (!mRecordEnabled) { |
| mUi->recButton->hide(); |
| } |
| |
| loadUi(); |
| } |
| |
| RecordMacroPage::~RecordMacroPage() { |
| sInstance = nullptr; |
| } |
| |
| // static |
| void RecordMacroPage::setAutomationAgent(const QAndroidAutomationAgent* agent) { |
| sAutomationAgent = agent; |
| } |
| |
| void RecordMacroPage::showEvent(QShowEvent* event) { |
| mAutomationMetrics = AutomationMetrics(); |
| } |
| |
| void RecordMacroPage::hideEvent(QHideEvent* event) { |
| if (!mAutomationMetrics.previewReplayCount && |
| !mAutomationMetrics.playbackCount) { |
| LOG(INFO) << "Dropping metrics, nothing to report."; |
| return; |
| } |
| |
| const uint64_t now = android::base::System::get()->getHighResTimeUs(); |
| |
| // Reset the metrics reporting limiter if enough time has passed. |
| if (mReportWindowStartUs + kReportWindowDurationUs < now) { |
| mReportWindowStartUs = now; |
| mReportWindowCount = 0; |
| } |
| |
| if (mReportWindowCount > kMaxReportsPerWindow) { |
| LOG(INFO) << "Dropping metrics, too many recent reports."; |
| return; |
| } |
| |
| ++mReportWindowCount; |
| |
| reportAllMetrics(); |
| } |
| |
| void RecordMacroPage::loadUi() { |
| // Clear all items. Might need to optimize this and keep track of existing. |
| mUi->macroList->clear(); |
| |
| // Lengths as QStrings have to be initialized here to use tr(). |
| mLengths = {{"Reset_position", tr("0:00")}, |
| {"Track_horizontal_plane", tr("0:16")}, |
| {"Track_vertical_plane", tr("0:12")}, |
| {"Walk_to_image_room", tr("0:14")}}; |
| |
| // Descriptions as QStrings have to be initialized here to use tr(). |
| mDescriptions = { |
| {"Reset_position", tr("Resets sensors to default.")}, |
| {"Track_horizontal_plane", tr("Circles around the rug.")}, |
| {"Track_vertical_plane", tr("Looks at the wall next to the tv.")}, |
| {"Walk_to_image_room", tr("Moves to the dining room.")}}; |
| |
| const std::string macrosPath = getMacrosDirectory(); |
| std::vector<std::string> macroFileNames = |
| System::get()->scanDirEntries(macrosPath); |
| |
| // For every macro, create a macroSavedItem with its name. |
| for (auto& macroName : macroFileNames) { |
| createMacroItem(macroName, true); |
| } |
| |
| if (mRecordEnabled) { |
| loadCustomMacros(); |
| } |
| |
| QObject::connect(&mTimer, &QTimer::timeout, this, |
| &RecordMacroPage::updateElapsedTime); |
| |
| QObject::connect(this, &RecordMacroPage::playbackFinishedSignal, this, |
| &RecordMacroPage::playbackFinished); |
| |
| setMacroUiState(MacroUiState::Waiting); |
| } |
| |
| void RecordMacroPage::on_playStopButton_clicked() { |
| // Stop and reset automation. |
| sAutomationAgent->stopPlayback(); |
| |
| QListWidgetItem* listItem = mUi->macroList->selectedItems().first(); |
| if (mState == MacroUiState::Playing) { |
| stopButtonClicked(listItem); |
| } else { |
| reportMacroPlayed(); |
| playButtonClicked(listItem); |
| } |
| } |
| |
| void RecordMacroPage::on_macroList_currentItemChanged( |
| QListWidgetItem* current, |
| QListWidgetItem* previous) { |
| if (current && previous) { |
| RecordMacroSavedItem* macroSavedItem = getItemWidget(previous); |
| macroSavedItem->macroSelected(false); |
| } |
| if (current) { |
| RecordMacroSavedItem* macroSavedItem = getItemWidget(current); |
| macroSavedItem->macroSelected(true); |
| } |
| } |
| |
| void RecordMacroPage::on_macroList_itemPressed(QListWidgetItem* listItem) { |
| const std::string macroName = getMacroNameFromItem(listItem); |
| |
| if (mMacroPlaying && mCurrentMacroName == macroName) { |
| setMacroUiState(MacroUiState::Playing); |
| showPreviewFrame(macroName); |
| } else { |
| setMacroUiState(MacroUiState::Selected); |
| showPreview(macroName); |
| } |
| } |
| |
| // For dragging and clicking outside the items in the item list. |
| void RecordMacroPage::on_macroList_itemSelectionChanged() { |
| if (mVideoPlayer && mVideoPlayer->isRunning()) { |
| mVideoPlayer->stop(); |
| } |
| |
| setMacroUiState(MacroUiState::Waiting); |
| } |
| |
| std::string RecordMacroPage::getMacrosDirectory() { |
| return PathUtils::join(System::get()->getLauncherDirectory(), "resources", |
| "macros"); |
| } |
| |
| std::string RecordMacroPage::getMacroPreviewsDirectory() { |
| return PathUtils::join(System::get()->getLauncherDirectory(), "resources", |
| "macroPreviews"); |
| } |
| |
| void RecordMacroPage::setMacroUiState(MacroUiState state) { |
| mState = state; |
| |
| switch (mState) { |
| case MacroUiState::Waiting: { |
| if (!mRecording) { |
| mUi->previewLabel->setText(tr("Select a macro to preview")); |
| mUi->previewLabel->show(); |
| } |
| mUi->previewOverlay->show(); |
| mUi->replayIcon->hide(); |
| mUi->playStopButton->setIcon(getIconForCurrentTheme("play_arrow")); |
| mUi->playStopButton->setProperty("themeIconName", "play_arrow"); |
| mUi->playStopButton->setText(tr("PLAY ")); |
| mUi->playStopButton->setEnabled(false); |
| break; |
| } |
| case MacroUiState::Selected: { |
| mUi->previewLabel->hide(); |
| mUi->previewOverlay->hide(); |
| mUi->replayIcon->hide(); |
| mUi->playStopButton->setIcon(getIconForCurrentTheme("play_arrow")); |
| mUi->playStopButton->setProperty("themeIconName", "play_arrow"); |
| mUi->playStopButton->setText(tr("PLAY ")); |
| mUi->playStopButton->setEnabled(true); |
| break; |
| } |
| case MacroUiState::PreviewFinished: { |
| mUi->previewLabel->setText(tr("Click anywhere to replay preview")); |
| mUi->previewLabel->show(); |
| mUi->previewOverlay->show(); |
| mUi->replayIcon->setPixmap(getIconForCurrentTheme("refresh").pixmap(QSize(36, 36))); |
| mUi->replayIcon->show(); |
| mUi->playStopButton->setIcon(getIconForCurrentTheme("play_arrow")); |
| mUi->playStopButton->setProperty("themeIconName", "play_arrow"); |
| mUi->playStopButton->setText(tr("PLAY ")); |
| mUi->playStopButton->setEnabled(true); |
| break; |
| } |
| case MacroUiState::Playing: { |
| mUi->previewLabel->setText(tr("Macro playing on the Emulator")); |
| mUi->previewLabel->show(); |
| mUi->previewOverlay->show(); |
| mUi->replayIcon->hide(); |
| mUi->playStopButton->setIcon(getIconForCurrentTheme("stop_red")); |
| mUi->playStopButton->setProperty("themeIconName", "stop_red"); |
| mUi->playStopButton->setText(tr("STOP ")); |
| mUi->playStopButton->setEnabled(true); |
| break; |
| } |
| case MacroUiState::PreRecording: { |
| mUi->playStopButton->setEnabled(false); |
| break; |
| } |
| case MacroUiState::Recording: { |
| mUi->playStopButton->setEnabled(false); |
| break; |
| } |
| } |
| |
| if (mRecordEnabled) { |
| setRecordState(); |
| } |
| } |
| |
| void RecordMacroPage::playButtonClicked(QListWidgetItem* listItem) { |
| RecordMacroSavedItem* macroSavedItem = getItemWidget(listItem); |
| macroSavedItem->setDisplayInfo(tr("Now playing...")); |
| if (mVideoPlayer && mVideoPlayer->isRunning()) { |
| mVideoPlayer->stop(); |
| } |
| |
| const std::string macroName = getMacroNameFromItem(listItem); |
| // Check if preset or custom. |
| std::string macroAbsolutePath; |
| if (macroSavedItem->getIsPreset()) { |
| macroAbsolutePath = PathUtils::join(getMacrosDirectory(), macroName); |
| } else { |
| macroAbsolutePath = |
| PathUtils::join(getCustomMacrosDirectory(), macroName); |
| } |
| |
| disableMacroItemsExcept(listItem); |
| |
| mCurrentMacroName = macroName; |
| mMacroPlaying = true; |
| mMacroItemPlaying = macroSavedItem; |
| mListItemPlaying = listItem; |
| setMacroUiState(MacroUiState::Playing); |
| |
| // Update every second. |
| mSec = 0; |
| mMacroItemPlaying->setDisplayTime(getTimerString(0)); |
| mTimer.start(1000); |
| |
| // Start timer for report. |
| mElapsedTimeTimer.start(); |
| |
| showPreviewFrame(macroName); |
| |
| auto result = sAutomationAgent->startPlaybackWithCallback( |
| macroAbsolutePath, &RecordMacroPage::stopCurrentMacro); |
| if (result.err()) { |
| std::ostringstream errString; |
| errString << result.unwrapErr(); |
| stopButtonClicked(listItem); |
| displayErrorBox(errString.str()); |
| return; |
| } |
| |
| if (macroSavedItem->getIsPreset()) { |
| reportPresetMacroPlayed(macroName); |
| } |
| } |
| |
| void RecordMacroPage::stopButtonClicked(QListWidgetItem* listItem) { |
| const std::string macroName = getMacroNameFromItem(listItem); |
| RecordMacroSavedItem* macroSavedItem = getItemWidget(listItem); |
| macroSavedItem->setDisplayInfo(mDescriptions[macroName]); |
| |
| enableMacroItems(); |
| |
| mMacroPlaying = false; |
| mUi->macroList->setCurrentItem(listItem); |
| setMacroUiState(MacroUiState::PreviewFinished); |
| |
| mTimer.stop(); |
| mMacroItemPlaying->setDisplayTime(mLengths[mCurrentMacroName]); |
| |
| reportTotalDuration(); |
| showPreviewFrame(macroName); |
| } |
| |
| void RecordMacroPage::showPreview(const std::string& macroName) { |
| // Guard for missing preview. |
| if (mRecordEnabled && !isPreviewAvailable(macroName)) { |
| mUi->previewLabel->setText(tr("No preview available.")); |
| mUi->previewLabel->show(); |
| mUi->previewOverlay->show(); |
| mUi->replayIcon->hide(); |
| return; |
| } |
| |
| const std::string previewPath = |
| PathUtils::join(getMacroPreviewsDirectory(), macroName + ".mp4"); |
| |
| auto videoPlayerNotifier = |
| std::unique_ptr<android::videoplayer::QtVideoPlayerNotifier>( |
| new android::videoplayer::QtVideoPlayerNotifier()); |
| connect(videoPlayerNotifier.get(), SIGNAL(updateWidget()), this, |
| SLOT(updatePreviewVideoView())); |
| connect(videoPlayerNotifier.get(), SIGNAL(videoStopped()), this, |
| SLOT(previewVideoPlayingFinished())); |
| mVideoPlayer = android::videoplayer::VideoPlayer::create( |
| previewPath, mUi->videoWidget, std::move(videoPlayerNotifier)); |
| |
| mVideoPlayer->scheduleRefresh(20); |
| mVideoPlayer->start(); |
| } |
| |
| RecordMacroSavedItem* RecordMacroPage::getItemWidget( |
| QListWidgetItem* listItem) { |
| return qobject_cast<RecordMacroSavedItem*>( |
| mUi->macroList->itemWidget(listItem)); |
| } |
| |
| void RecordMacroPage::updatePreviewVideoView() { |
| mUi->videoWidget->repaint(); |
| } |
| |
| void RecordMacroPage::previewVideoPlayingFinished() { |
| setMacroUiState(MacroUiState::PreviewFinished); |
| |
| QListWidgetItem* listItem = mUi->macroList->selectedItems().first(); |
| const std::string macroName = getMacroNameFromItem(listItem); |
| showPreviewFrame(macroName); |
| } |
| |
| void RecordMacroPage::mousePressEvent(QMouseEvent* event) { |
| if (mState == MacroUiState::PreviewFinished) { |
| QListWidgetItem* listItem = mUi->macroList->selectedItems().first(); |
| const std::string macroName = getMacroNameFromItem(listItem); |
| setMacroUiState(MacroUiState::Selected); |
| |
| showPreview(macroName); |
| reportPreviewPlayedAgain(); |
| } |
| } |
| |
| void RecordMacroPage::disableMacroItemsExcept(QListWidgetItem* listItem) { |
| // Make selection show that macro is playing. |
| SettingsTheme theme = getSelectedTheme(); |
| mUi->macroList->setStyleSheet( |
| "QListWidget::item:focus, QListView::item:selected { " |
| "background-color: " + |
| Ui::stylesheetValues(theme)[Ui::MACRO_BKG_COLOR_VAR] + "}"); |
| |
| for (int i = 0; i < mUi->macroList->count(); ++i) { |
| QListWidgetItem* item = mUi->macroList->item(i); |
| RecordMacroSavedItem* macroItem = getItemWidget(item); |
| macroItem->editEnabled(false); |
| if (item != listItem) { |
| item->setFlags(item->flags() & ~Qt::ItemIsEnabled); |
| macroItem->setEnabled(false); |
| } |
| } |
| } |
| |
| void RecordMacroPage::disableMacroItems() { |
| for (int i = 0; i < mUi->macroList->count(); ++i) { |
| QListWidgetItem* item = mUi->macroList->item(i); |
| item->setFlags(item->flags() & ~Qt::ItemIsEnabled); |
| RecordMacroSavedItem* macroItem = getItemWidget(item); |
| macroItem->setEnabled(false); |
| } |
| } |
| |
| void RecordMacroPage::enableMacroItems() { |
| // Return selection to normal. |
| mUi->macroList->setStyleSheet(""); |
| |
| for (int i = 0; i < mUi->macroList->count(); ++i) { |
| QListWidgetItem* item = mUi->macroList->item(i); |
| item->setFlags(item->flags() | Qt::ItemIsEnabled); |
| RecordMacroSavedItem* macroItem = getItemWidget(item); |
| macroItem->setEnabled(true); |
| if (!macroItem->getIsPreset()) { |
| macroItem->editEnabled(true); |
| } |
| } |
| } |
| |
| void RecordMacroPage::showPreviewFrame(const std::string& macroName) { |
| // Guard for missing preview. |
| if (mRecordEnabled && !isPreviewAvailable(macroName)) { |
| return; |
| } |
| |
| const std::string previewPath = |
| PathUtils::join(getMacroPreviewsDirectory(), macroName + ".mp4"); |
| |
| mVideoInfo.reset( |
| new android::videoplayer::VideoInfo(mUi->videoWidget, previewPath)); |
| connect(mVideoInfo.get(), SIGNAL(updateWidget()), this, |
| SLOT(updatePreviewVideoView())); |
| |
| mVideoInfo->show(); |
| } |
| |
| std::string RecordMacroPage::getMacroNameFromItem(QListWidgetItem* listItem) { |
| QVariant data = listItem->data(Qt::UserRole); |
| QString macroName = data.toString(); |
| return macroName.toUtf8().constData(); |
| } |
| |
| void RecordMacroPage::updateElapsedTime() { |
| mSec++; |
| switch (mState) { |
| case MacroUiState::PreRecording: { |
| // Countdown from 3 to 0. |
| QString qs = tr("%1").arg(3 - mSec); |
| mUi->countLabel->setText(qs); |
| if (mSec == 3) { |
| startRecording(); |
| return; |
| } |
| break; |
| } |
| case MacroUiState::Recording: { |
| QString qs = tr(SECONDS_RECORDING).arg(mSec); |
| mUi->recordingLabel->setText(qs); |
| break; |
| } |
| default: { |
| mMacroItemPlaying->setDisplayTime(getTimerString(mSec)); |
| break; |
| } |
| } |
| mTimer.start(1000); |
| } |
| |
| QString RecordMacroPage::getTimerString(int seconds) { |
| QString qs = tr("%1:%2%3 / ") |
| .arg(seconds / 60) |
| .arg(seconds % 60 > 9 ? "" : "0") |
| .arg(seconds % 60); |
| qs.append(mLengths[mCurrentMacroName]); |
| return qs; |
| } |
| |
| // static |
| void RecordMacroPage::stopCurrentMacro() { |
| sInstance->emitPlaybackFinished(); |
| } |
| |
| void RecordMacroPage::emitPlaybackFinished() { |
| emit playbackFinishedSignal(); |
| } |
| |
| void RecordMacroPage::playbackFinished() { |
| stopButtonClicked(mListItemPlaying); |
| } |
| |
| void RecordMacroPage::reportMacroPlayed() { |
| mAutomationMetrics.playbackCount++; |
| } |
| |
| void RecordMacroPage::reportPreviewPlayedAgain() { |
| mAutomationMetrics.previewReplayCount++; |
| } |
| |
| void RecordMacroPage::reportTotalDuration() { |
| mAutomationMetrics.totalDurationMs += mElapsedTimeTimer.elapsed(); |
| } |
| |
| void RecordMacroPage::reportPresetMacroPlayed(const std::string& macroName) { |
| EmulatorAutomationPresetMacro presetMacro; |
| |
| if (macroName == "Reset_position") { |
| presetMacro = |
| EmulatorAutomation::EMULATOR_AUTOMATION_PRESET_MACRO_RESET; |
| } else if (macroName == "Track_horizontal_plane") { |
| presetMacro = EmulatorAutomation:: |
| EMULATOR_AUTOMATION_PRESET_MACRO_TRACK_HORIZONTAL; |
| } else if (macroName == "Track_vertical_plane") { |
| presetMacro = EmulatorAutomation:: |
| EMULATOR_AUTOMATION_PRESET_MACRO_TRACK_VERTICAL; |
| } else if (macroName == "Walk_to_image_room") { |
| presetMacro = |
| EmulatorAutomation::EMULATOR_AUTOMATION_PRESET_MACRO_IMAGE_ROOM; |
| } else { |
| return; |
| } |
| |
| mAutomationMetrics.presetsPlayed.push_back(presetMacro); |
| } |
| |
| void RecordMacroPage::reportAllMetrics() { |
| EmulatorAutomation metrics; |
| |
| metrics.set_macro_playback_count(mAutomationMetrics.playbackCount); |
| metrics.set_preview_replay_count(mAutomationMetrics.previewReplayCount); |
| metrics.set_total_duration_ms(mAutomationMetrics.totalDurationMs); |
| for (auto presetMacro : mAutomationMetrics.presetsPlayed) { |
| metrics.add_played_preset_macro(presetMacro); |
| } |
| |
| MetricsReporter::get().report( |
| [metrics](android_studio::AndroidStudioEvent* event) { |
| auto automation = |
| event->mutable_emulator_details()->mutable_automation(); |
| automation->CopyFrom(metrics); |
| }); |
| } |
| |
| void RecordMacroPage::on_recButton_clicked() { |
| if (!mRecording) { |
| setMacroUiState(MacroUiState::PreRecording); |
| } else { |
| stopRecording(); |
| } |
| } |
| |
| void RecordMacroPage::startRecording() { |
| if (mVideoPlayer && mVideoPlayer->isRunning()) { |
| mVideoPlayer->stop(); |
| } |
| disableMacroItems(); |
| mRecording = true; |
| |
| const std::string macrosLocation = getCustomMacrosDirectory(); |
| const std::string placeholderPath = |
| PathUtils::join(macrosLocation, "placeholder_name"); |
| |
| auto result = sAutomationAgent->startRecording(placeholderPath); |
| if (result.err()) { |
| std::ostringstream errString; |
| errString << result.unwrapErr(); |
| displayErrorBox(errString.str()); |
| return; |
| } |
| |
| setMacroUiState(MacroUiState::Recording); |
| } |
| |
| void RecordMacroPage::stopRecording() { |
| mTimer.stop(); |
| enableMacroItems(); |
| mRecording = false; |
| |
| auto result = sAutomationAgent->stopRecording(); |
| if (result.err()) { |
| std::ostringstream errString; |
| errString << result.unwrapErr(); |
| displayErrorBox(errString.str()); |
| return; |
| } |
| |
| std::string newName = displayNameMacroBox(); |
| |
| const std::string macrosLocation = getCustomMacrosDirectory(); |
| const std::string oldPath = |
| PathUtils::join(macrosLocation, "placeholder_name"); |
| if (!newName.empty()) { |
| // Rename file. |
| sAutomationAgent->setMacroName(newName, oldPath); |
| newName = QDateTime::currentDateTime() |
| .toString("yyyyMMdd-hhmmss-") |
| .toUtf8() |
| .constData() + |
| newName; |
| newName.append(".emu-macro"); |
| const std::string newPath = PathUtils::join(macrosLocation, newName); |
| if (std::rename(oldPath.c_str(), newPath.c_str()) != 0) { |
| LOG(ERROR) << "Renaming file failed."; |
| std::remove(oldPath.c_str()); |
| } else { |
| createMacroItem(newName, false); |
| } |
| } else { |
| // Delete file. |
| std::remove(oldPath.c_str()); |
| } |
| |
| setMacroUiState(MacroUiState::Waiting); |
| } |
| |
| void RecordMacroPage::setRecordState() { |
| if (!mRecording) { |
| mUi->recButton->setText(tr("RECORD NEW ")); |
| mUi->recButton->setIcon(getIconForCurrentTheme("recordCircle")); |
| mUi->recButton->setProperty("themeIconName", "recordCircle"); |
| } |
| mUi->stackedWidget->setCurrentIndex(0); |
| |
| switch (mState) { |
| case MacroUiState::Waiting: { |
| if (!mMacroPlaying) { |
| mUi->recButton->setEnabled(true); |
| } |
| break; |
| } |
| case MacroUiState::Selected: { |
| mUi->recButton->setEnabled(true); |
| break; |
| } |
| case MacroUiState::PreviewFinished: { |
| mUi->recButton->setEnabled(true); |
| break; |
| } |
| case MacroUiState::Playing: { |
| mUi->recButton->setEnabled(false); |
| break; |
| } |
| case MacroUiState::PreRecording: { |
| mUi->stackedWidget->setCurrentIndex(1); |
| mUi->recButton->setEnabled(false); |
| mUi->recordingLabel->setText(tr("Recording will start in")); |
| mUi->cancelButton->show(); |
| // Update every second |
| mUi->countLabel->show(); |
| mUi->countLabel->setText(tr("3")); |
| mSec = 0; |
| mTimer.start(1000); |
| break; |
| } |
| case MacroUiState::Recording: { |
| mUi->stackedWidget->setCurrentIndex(1); |
| mUi->countLabel->hide(); |
| mUi->cancelButton->hide(); |
| mUi->recButton->setEnabled(true); |
| mUi->recButton->setIcon(getIconForCurrentTheme("stop_red")); |
| mUi->recButton->setProperty("themeIconName", "stop_red"); |
| mUi->recButton->setText(tr("STOP RECORDING ")); |
| // Update every second |
| mUi->recordingLabel->setText(tr(SECONDS_RECORDING).arg(0)); |
| mSec = 0; |
| mTimer.start(1000); |
| break; |
| } |
| } |
| } |
| |
| std::string RecordMacroPage::getCustomMacrosDirectory() { |
| const std::string appDataLocation = |
| QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) |
| .toUtf8() |
| .constData(); |
| const std::string macrosLocation = |
| PathUtils::join(appDataLocation, "macros"); |
| QDir dir(QString::fromStdString(macrosLocation)); |
| if (!dir.exists()) { |
| dir.mkpath("."); |
| } |
| return macrosLocation; |
| } |
| |
| void RecordMacroPage::displayErrorBox(const std::string& errorStr) { |
| QMessageBox msgBox; |
| msgBox.setIcon(QMessageBox::Warning); |
| msgBox.setText(tr("An error ocurred.")); |
| msgBox.setInformativeText(errorStr.c_str()); |
| msgBox.setDefaultButton(QMessageBox::Save); |
| msgBox.exec(); |
| } |
| |
| std::string RecordMacroPage::displayNameMacroBox() { |
| QWidget* placeholderWidget = new QWidget; |
| QVBoxLayout* dialogLayout = new QVBoxLayout(placeholderWidget); |
| |
| dialogLayout->addWidget(new QLabel(tr("Macro name"))); |
| QLineEdit* name = new QLineEdit(this); |
| name->setMaxLength(32); |
| name->setText(tr("My Macro")); |
| name->selectAll(); |
| dialogLayout->addWidget(name); |
| |
| QDialogButtonBox* buttonBox = new QDialogButtonBox( |
| QDialogButtonBox::Save | QDialogButtonBox::Cancel, Qt::Horizontal); |
| dialogLayout->addWidget(buttonBox); |
| |
| QDialog nameDialog(this); |
| |
| connect(buttonBox, SIGNAL(rejected()), &nameDialog, SLOT(reject())); |
| connect(buttonBox, SIGNAL(accepted()), &nameDialog, SLOT(accept())); |
| |
| nameDialog.setWindowTitle(tr("Enter a name for your recording")); |
| nameDialog.setLayout(dialogLayout); |
| nameDialog.resize(300, nameDialog.height()); |
| |
| int selection; |
| QString newName; |
| QRegExp re("\\w+"); |
| // Check that name is [a-zA-Z0-9_] after trimming. |
| do { |
| selection = nameDialog.exec(); |
| newName = name->text(); |
| newName.replace("_", " "); |
| newName = newName.simplified(); |
| newName.replace(" ", "_"); |
| } while (selection == QDialog::Accepted && !re.exactMatch(newName)); |
| |
| if (selection == QDialog::Rejected) { |
| return ""; |
| } |
| return newName.toUtf8().constData(); |
| } |
| |
| void RecordMacroPage::loadCustomMacros() { |
| const std::string macrosPath = getCustomMacrosDirectory(); |
| std::vector<std::string> macroFileNames = |
| System::get()->scanDirEntries(macrosPath); |
| |
| for (auto& macroName : macroFileNames) { |
| // Extension check. |
| QFileInfo fi(tr(macroName.c_str())); |
| if (fi.suffix() == "emu-macro") { |
| createMacroItem(macroName, false); |
| } |
| } |
| } |
| |
| void RecordMacroPage::createMacroItem(std::string& macroName, bool isPreset) { |
| QListWidgetItem* listItem = new QListWidgetItem(mUi->macroList); |
| QVariant macroNameData(QString::fromStdString(macroName)); |
| listItem->setData(Qt::UserRole, macroNameData); |
| |
| RecordMacroSavedItem* macroSavedItem = new RecordMacroSavedItem(); |
| if (isPreset) { |
| macroSavedItem->editEnabled(false); |
| macroSavedItem->setIsPreset(true); |
| macroSavedItem->setDisplayInfo(mDescriptions[macroName]); |
| macroSavedItem->setDisplayTime(mLengths[macroName]); |
| macroName.append(" (Preset macro)"); |
| } else { |
| const std::string macrosLocation = getCustomMacrosDirectory(); |
| const std::string filePath = PathUtils::join(macrosLocation, macroName); |
| uint64_t durationNs = sAutomationAgent->getDurationNs(filePath); |
| int durationS = durationNs / 1000000000; |
| QString qs = tr("%1:%2%3") |
| .arg(durationS / 60) |
| .arg(durationS % 60 > 9 ? "" : "0") |
| .arg(durationS % 60); |
| mLengths[macroName] = qs; |
| |
| macroSavedItem->setDisplayTime(mLengths[macroName]); |
| macroName = sAutomationAgent->getMacroName(filePath); |
| connect(macroSavedItem, |
| SIGNAL(editButtonClickedSignal(RecordMacroSavedItem*)), this, |
| SLOT(editButtonClicked(RecordMacroSavedItem*))); |
| } |
| std::replace(macroName.begin(), macroName.end(), '_', ' '); |
| macroSavedItem->setName(macroName.c_str()); |
| |
| listItem->setSizeHint(QSize(0, 50)); |
| |
| mUi->macroList->addItem(listItem); |
| mUi->macroList->setItemWidget(listItem, macroSavedItem); |
| } |
| |
| bool RecordMacroPage::isPreviewAvailable(const std::string& macroName) { |
| // Only check if it is custom for the moment. |
| return macroName.find(".emu-macro") == std::string::npos; |
| } |
| |
| void RecordMacroPage::editButtonClicked(RecordMacroSavedItem* macroItem) { |
| RecordMacroEditDialog editDialog(this); |
| editDialog.setCurrentName(QString::fromStdString(macroItem->getName())); |
| |
| int selection = editDialog.exec(); |
| |
| if (selection == ButtonClicked::Delete) { |
| deleteMacroItem(macroItem); |
| } else if (selection == QDialog::Accepted) { |
| // Update macro item. |
| QString newName = editDialog.getNewName(); |
| macroItem->setName(newName); |
| |
| // Update file header. |
| const std::string macroName = newName.toUtf8().constData(); |
| QListWidgetItem* listItem = findListItemFromWidget(macroItem); |
| if (!listItem) { |
| displayErrorBox("Edit failed."); |
| return; |
| } |
| const std::string macrosLocation = getCustomMacrosDirectory(); |
| const std::string path = |
| PathUtils::join(macrosLocation, getMacroNameFromItem(listItem)); |
| sAutomationAgent->setMacroName(macroName, path); |
| } |
| } |
| |
| QListWidgetItem* RecordMacroPage::findListItemFromWidget( |
| RecordMacroSavedItem* macroItem) { |
| for (int i = 0; i < mUi->macroList->count(); ++i) { |
| QListWidgetItem* item = mUi->macroList->item(i); |
| if (macroItem == getItemWidget(item)) { |
| return item; |
| } |
| } |
| return NULL; |
| } |
| |
| void RecordMacroPage::deleteMacroItem(RecordMacroSavedItem* macroItem) { |
| QString macroName = QString::fromStdString(macroItem->getName()); |
| QMessageBox msgBox(QMessageBox::Question, tr("Delete macro"), |
| tr("Do you want to permanently delete<br>macro \"%1\"?") |
| .arg(macroName), |
| QMessageBox::Cancel, this); |
| QPushButton* deleteButton = msgBox.addButton(QMessageBox::Ok); |
| deleteButton->setText(tr("Delete")); |
| |
| int selection = msgBox.exec(); |
| if (selection == QMessageBox::Ok) { |
| QListWidgetItem* listItem = findListItemFromWidget(macroItem); |
| const std::string name = getMacroNameFromItem(listItem); |
| mUi->macroList->model()->removeRow(mUi->macroList->row(listItem)); |
| const std::string macrosLocation = getCustomMacrosDirectory(); |
| const std::string path = PathUtils::join(macrosLocation, name); |
| if (std::remove(path.c_str()) != 0) { |
| displayErrorBox("Deletion failed."); |
| } |
| } |
| } |
| |
| void RecordMacroPage::on_cancelButton_clicked() { |
| mTimer.stop(); |
| enableMacroItems(); |
| setMacroUiState(MacroUiState::Waiting); |
| } |