// Copyright (C) 2022 The Qt Company Ltd // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "squishperspective.h" #include "squishtools.h" #include "squishtr.h" #include "squishxmloutputhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Squish { namespace Internal { enum class IconType { StopRecord, Play, Pause, StepIn, StepOver, StepReturn, Stop, Inspect }; static QIcon iconForType(IconType type) { static const Utils::Icon inspectIcon({{":/squish/images/picker.png", Utils::Theme::IconsBaseColor}}); switch (type) { case IconType::StopRecord: return Debugger::Icons::RECORD_ON.icon(); case IconType::Play: return Debugger::Icons::DEBUG_CONTINUE_SMALL_TOOLBAR.icon(); case IconType::Pause: return Utils::Icons::INTERRUPT_SMALL.icon(); case IconType::StepIn: return Debugger::Icons::STEP_INTO_TOOLBAR.icon(); case IconType::StepOver: return Debugger::Icons::STEP_OVER_TOOLBAR.icon(); case IconType::StepReturn: return Debugger::Icons::STEP_OUT_TOOLBAR.icon(); case IconType::Stop: return Utils::Icons::STOP_SMALL.icon(); case IconType::Inspect: return inspectIcon.icon(); } return QIcon(); } static QString customStyleSheet(bool extended) { static const QString red = Utils::creatorColor( Utils::Theme::ProgressBarColorError).name(); static const QString green = Utils::creatorColor( Utils::Theme::ProgressBarColorFinished).name(); if (!extended) return "QProgressBar {text-align:left; border:0px}"; return QString("QProgressBar {background:%1; text-align:left; border:0px}" "QProgressBar::chunk {background:%2; border:0px}").arg(red, green); } static QStringList splitDebugContent(const QString &content) { if (content.isEmpty()) return {}; QStringList symbols; int delimiter = -1; int start = 0; do { delimiter = content.indexOf(',', delimiter + 1); if (delimiter > 0 && content.at(delimiter - 1) == '\\') continue; symbols.append(content.mid(start, delimiter - start)); start = delimiter + 1; } while (delimiter >= 0); return symbols; } QVariant LocalsItem::data(int column, int role) const { if (role == Qt::DisplayRole) { switch (column) { case 0: return name; case 1: return type; case 2: return value; } } return TreeItem::data(column, role); } QVariant InspectedObjectItem::data(int column, int role) const { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { switch (column) { case 0: return value; case 1: return type; } } return TreeItem::data(column, role); } QVariant InspectedPropertyItem::data(int column, int role) const { if (role ==Qt::DisplayRole || role == Qt::ToolTipRole) { switch (column) { case 0: return name; case 1: return value; } } return TreeItem::data(column, role); } void InspectedPropertyItem::parseAndUpdateChildren() { const char open = '{'; const char close = '}'; const char delimiter =','; const char eq = '='; if (!value.startsWith(open) || !value.endsWith(close)) // only parse multi-property content return; int start = 1; int end = value.size() - 1; do { int endOfName = value.indexOf(eq, start); QTC_ASSERT(endOfName != -1, return); int innerStart = endOfName + 2; QTC_ASSERT(innerStart < end, return); const QString name = value.mid(start, endOfName - start).trimmed(); if (value.at(innerStart) != open) { int endOfItemValue = value.indexOf(delimiter, innerStart); if (endOfItemValue == -1) endOfItemValue = end; const QString content = value.mid(innerStart, endOfItemValue - innerStart).trimmed(); appendChild(new InspectedPropertyItem(name, content)); start = endOfItemValue + 1; } else { int openedBraces = 1; // advance until item's content is complete int pos = innerStart; do { if (++pos > end) break; if (value.at(pos) == open) { ++openedBraces; continue; } if (value.at(pos) == close) { --openedBraces; if (openedBraces == 0) break; } } while (pos < end); ++pos; QTC_ASSERT(pos < end, return); const QString content = value.mid(innerStart, pos - innerStart).trimmed(); appendChild(new InspectedPropertyItem(name, content)); start = pos + 1; } } while (start < end); } class SquishControlBar : public QDialog { public: explicit SquishControlBar(SquishPerspective *perspective); void increaseFailCounter() { ++m_fails; updateProgressBar(); } void increasePassCounter() { ++m_passes; updateProgressBar(); } void updateProgressText(const QString &label); protected: void closeEvent(QCloseEvent *) override { m_perspective->onStopTriggered(); } void resizeEvent(QResizeEvent *) override { updateProgressText(m_labelText); } private: void updateProgressBar(); SquishPerspective *m_perspective = nullptr; QToolBar *m_toolBar = nullptr; QProgressBar *m_progress = nullptr; QString m_labelText; int m_passes = 0; int m_fails = 0; }; SquishControlBar::SquishControlBar(SquishPerspective *perspective) : QDialog() , m_perspective(perspective) { setWindowTitle(Tr::tr("Control Bar")); setWindowFlags(Qt::Tool | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addWidget(m_toolBar = new QToolBar(this)); // for now m_toolBar->addAction(perspective->m_stopRecordAction); m_toolBar->addAction(perspective->m_pausePlayAction); m_toolBar->addAction(perspective->m_stopAction); mainLayout->addWidget(m_progress = new QProgressBar(this)); m_progress->setMinimumHeight(48); m_progress->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding); m_progress->setStyleSheet(customStyleSheet(false)); m_progress->setFormat(QString()); m_progress->setValue(0); QPalette palette = Utils::creatorTheme()->palette(); m_progress->setPalette(palette); setLayout(mainLayout); } void SquishControlBar::updateProgressBar() { const int allCounted = m_passes + m_fails; if (allCounted == 0) return; if (allCounted == 1) { QPalette palette = m_progress->palette(); palette.setColor(QPalette::Text, Qt::black); m_progress->setStyleSheet(customStyleSheet(true)); m_progress->setPalette(palette); } m_progress->setRange(0, allCounted); m_progress->setValue(m_passes); } void SquishControlBar::updateProgressText(const QString &label) { const QString status = m_progress->fontMetrics().elidedText(label, Qt::ElideMiddle, m_progress->width()); if (!status.isEmpty()) { m_labelText = label; m_progress->setFormat(status); } } SquishPerspective::SquishPerspective() : Utils::Perspective("Squish.Perspective", Tr::tr("Squish")) { Core::ICore::addPreCloseListener([this]{ destroyControlBar(); return true; }); } void SquishPerspective::initPerspective() { m_stopRecordAction = new QAction(this); m_stopRecordAction->setIcon(iconForType(IconType::StopRecord)); m_stopRecordAction->setToolTip(Tr::tr("Stop Recording") + "\n\n" + Tr::tr("Ends the recording session, " "saving all commands to the script file.")); m_stopRecordAction->setEnabled(false); m_pausePlayAction = new QAction(this); m_pausePlayAction->setIcon(iconForType(IconType::Pause)); m_pausePlayAction->setToolTip(Tr::tr("Interrupt")); m_pausePlayAction->setEnabled(false); m_stepInAction = new QAction(this); m_stepInAction->setIcon(iconForType(IconType::StepIn)); m_stepInAction->setToolTip(Tr::tr("Step Into")); m_stepInAction->setEnabled(false); m_stepOverAction = new QAction(this); m_stepOverAction->setIcon(iconForType(IconType::StepOver)); m_stepOverAction->setToolTip(Tr::tr("Step Over")); m_stepOverAction->setEnabled(false); m_stepOutAction = new QAction(this); m_stepOutAction->setIcon(iconForType(IconType::StepReturn)); m_stepOutAction->setToolTip(Tr::tr("Step Out")); m_stepOutAction->setEnabled(false); m_stopAction = Debugger::createStopAction(); m_stopAction->setEnabled(false); m_inspectAction = new QAction(this); m_inspectAction->setIcon(iconForType(IconType::Inspect)); m_inspectAction->setToolTip(Tr::tr("Inspect")); m_inspectAction->setEnabled(false); QVBoxLayout *localsMainLayout = new QVBoxLayout; localsMainLayout->setContentsMargins(0, 0, 0, 0); localsMainLayout->setSpacing(1); m_localsModel.setHeader({Tr::tr("Name"), Tr::tr("Type"), Tr::tr("Value")}); auto localsView = new Utils::TreeView; localsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); localsView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); localsView->setModel(&m_localsModel); localsView->setRootIsDecorated(true); localsMainLayout->addWidget(localsView); QWidget *localsWidget = new QWidget; localsWidget->setObjectName("SquishLocalsView"); localsWidget->setWindowTitle(Tr::tr("Squish Locals")); localsWidget->setLayout(localsMainLayout); QVBoxLayout *objectsMainLayout = new QVBoxLayout; objectsMainLayout->setContentsMargins(0, 0, 0, 0); objectsMainLayout->setSpacing(1); m_objectsModel.setHeader({Tr::tr("Object"), Tr::tr("Type")}); m_objectsView = new Utils::TreeView; m_objectsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_objectsView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_objectsView->setModel(&m_objectsModel); m_objectsView->setRootIsDecorated(true); objectsMainLayout->addWidget(m_objectsView); QWidget *objectWidget = new QWidget; objectWidget->setObjectName("SquishObjectsView"); objectWidget->setWindowTitle(Tr::tr("Squish Objects")); objectWidget->setLayout(objectsMainLayout); QVBoxLayout *propertiesMainLayout = new QVBoxLayout; propertiesMainLayout->setContentsMargins(0, 0, 0, 0); propertiesMainLayout->setSpacing(1); m_propertiesModel.setHeader({Tr::tr("Property"), Tr::tr("Value")}); auto propertiesView = new Utils::TreeView; propertiesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); propertiesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); propertiesView->setModel(&m_propertiesModel); propertiesView->setRootIsDecorated(true); propertiesMainLayout->addWidget(propertiesView); QWidget *propertiesWidget = new QWidget; propertiesWidget->setObjectName("SquishPropertiesView"); propertiesWidget->setWindowTitle(Tr::tr("Squish Object Properties")); propertiesWidget->setLayout(propertiesMainLayout); addToolBarAction(m_pausePlayAction); addToolBarAction(m_stepInAction); addToolBarAction(m_stepOverAction); addToolBarAction(m_stepOutAction); addToolBarAction(m_stopAction); addToolbarSeparator(); addToolBarAction(m_inspectAction); addToolbarSeparator(); m_status = new QLabel; addToolBarWidget(m_status); addWindow(objectWidget, Perspective::SplitVertical, nullptr); addWindow(propertiesWidget, Perspective::SplitHorizontal, objectWidget); addWindow(localsWidget, Perspective::AddToTab, nullptr, true, Qt::RightDockWidgetArea); connect(m_pausePlayAction, &QAction::triggered, this, &SquishPerspective::onPausePlayTriggered); connect(m_stepInAction, &QAction::triggered, this, [this] { emit runRequested(StepMode::StepIn); }); connect(m_stepOverAction, &QAction::triggered, this, [this] { emit runRequested(StepMode::StepOver); }); connect(m_stepOutAction, &QAction::triggered, this, [this] { emit runRequested(StepMode::StepOut); }); connect(m_stopAction, &QAction::triggered, this, &SquishPerspective::onStopTriggered); connect(m_stopRecordAction, &QAction::triggered, this, &SquishPerspective::onStopRecordTriggered); connect(m_inspectAction, &QAction::triggered, this, [this]{ m_inspectAction->setEnabled(false); emit inspectTriggered(); }); connect(SquishTools::instance(), &SquishTools::localsUpdated, this, &SquishPerspective::onLocalsUpdated); connect(localsView, &QTreeView::expanded, this, [this](const QModelIndex &idx) { LocalsItem *item = m_localsModel.itemForIndex(idx); if (QTC_GUARD(item)) { if (item->expanded) return; item->expanded = true; SquishTools::instance()->requestExpansion(item->name); } }); connect(SquishTools::instance(), &SquishTools::objectPicked, this, &SquishPerspective::onObjectPicked); connect(SquishTools::instance(), &SquishTools::updateChildren, this, &SquishPerspective::onUpdateChildren); connect(SquishTools::instance(), &SquishTools::propertiesFetched, this, &SquishPerspective::onPropertiesFetched); connect(SquishTools::instance(), &SquishTools::autIdRetrieved, this, [this]{ m_autIdKnown = true; m_inspectAction->setEnabled(true); }); connect(m_objectsView, &QTreeView::expanded, this, [this](const QModelIndex &idx) { InspectedObjectItem *item = m_objectsModel.itemForIndex(idx); if (QTC_GUARD(item)) { if (item->expanded) return; item->expanded = true; SquishTools::instance()->requestExpansionForObject(item->fullName); } }); connect(m_objectsView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex ¤t){ m_propertiesModel.clear(); InspectedObjectItem *item = m_objectsModel.itemForIndex(current); if (!item) return; SquishTools::instance()->requestPropertiesForObject(item->fullName); }); } void SquishPerspective::onStopTriggered() { m_stopRecordAction->setEnabled(false); m_pausePlayAction->setEnabled(false); m_stopAction->setEnabled(false); m_inspectAction->setEnabled(false); m_autIdKnown = false; emit stopRequested(); } void SquishPerspective::onStopRecordTriggered() { m_stopRecordAction->setEnabled(false); m_pausePlayAction->setEnabled(false); m_stopAction->setEnabled(false); m_inspectAction->setEnabled(false); m_autIdKnown = false; emit stopRecordRequested(); } void SquishPerspective::onPausePlayTriggered() { if (m_mode == Interrupted) emit runRequested(StepMode::Continue); else if (m_mode == Running) emit interruptRequested(); else qDebug() << "###state: " << m_mode; } void SquishPerspective::onLocalsUpdated(const QString &output) { static const QRegularExpression regex("\\+(?.+):\\{(?.*)\\}"); static const QRegularExpression inner("(?.+)#(?\\+*+)(?[^=]+)(=(?.+))?"); const QRegularExpressionMatch match = regex.match(output); LocalsItem *parent = nullptr; bool singleSymbol = match.hasMatch(); if (singleSymbol) { const QString name = match.captured("name"); parent = m_localsModel.findNonRootItem([name](LocalsItem *it) { return it->name == name; }); if (!parent) return; parent->removeChildren(); } else { m_localsModel.clear(); parent = m_localsModel.rootItem(); } const QStringList symbols = splitDebugContent(singleSymbol ? match.captured("content") : output); for (const QString &part : symbols) { const QRegularExpressionMatch iMatch = inner.match(part); QTC_ASSERT(iMatch.hasMatch(), qDebug() << part; continue); if (iMatch.captured("value").startsWith('<')) continue; LocalsItem *l = new LocalsItem(iMatch.captured("name"), iMatch.captured("type"), iMatch.captured("value").replace("\\,", ",")); // TODO if (!iMatch.captured("exp").isEmpty()) l->appendChild(new LocalsItem); // add pseudo child parent->appendChild(l); } } void SquishPerspective::onObjectPicked(const QString &output) { // "+{container=':o_QQuickView' text='2' type='Text' unnamed='1' visible='true'}\tQQuickText_QML_1" static const QRegularExpression regex("^(?[-+])(?\\{.*\\})\t(?.+)$"); const QRegularExpressionMatch match = regex.match(output); if (!match.hasMatch()) return; const QString content = match.captured("content"); m_objectsModel.clear(); InspectedObjectItem *parent = m_objectsModel.rootItem(); InspectedObjectItem *obj = new InspectedObjectItem(content, match.captured("type")); obj->fullName = content; if (match.captured("exp") == "+") obj->appendChild(new InspectedObjectItem); // add pseudo child parent->appendChild(obj); m_inspectAction->setEnabled(true); const QModelIndex idx = m_objectsModel.indexForItem(obj); if (idx.isValid()) m_objectsView->setCurrentIndex(idx); } void SquishPerspective::onUpdateChildren(const QString &name, const QStringList &children) { InspectedObjectItem *item = m_objectsModel.findNonRootItem([name](InspectedObjectItem *it) { return it->fullName == name; }); if (!item) return; item->removeChildren(); // remove former dummy child static const QRegularExpression regex("(?[-+])(?.+)\t(?.+)"); for (const QString &child : children) { const QRegularExpressionMatch match = regex.match(child); QTC_ASSERT(match.hasMatch(), continue); const QString symbolicName = match.captured("symbolicName"); auto childItem = new InspectedObjectItem(symbolicName, match.captured("type")); childItem->fullName = name + '.' + symbolicName; childItem->appendChild(new InspectedObjectItem); // add dummy child item->appendChild(childItem); } } void SquishPerspective::onPropertiesFetched(const QStringList &properties) { static const QRegularExpression regex("(?.+)=(?[-+])(?.*)"); for (const QString &line : properties) { const QRegularExpressionMatch match = regex.match(line); QTC_ASSERT(match.hasMatch(), continue); auto item = new InspectedPropertyItem(match.captured("name"), match.captured("content")); m_propertiesModel.rootItem()->appendChild(item); } } void SquishPerspective::updateStatus(const QString &status) { m_status->setText(status); if (m_controlBar) m_controlBar->updateProgressText(status); } void SquishPerspective::showControlBar(SquishXmlOutputHandler *xmlOutputHandler) { QTC_ASSERT(!m_controlBar, return); m_controlBar = new SquishControlBar(this); if (xmlOutputHandler) { connect(xmlOutputHandler, &SquishXmlOutputHandler::increasePassCounter, m_controlBar, &SquishControlBar::increasePassCounter); connect(xmlOutputHandler, &SquishXmlOutputHandler::increaseFailCounter, m_controlBar, &SquishControlBar::increaseFailCounter); connect (xmlOutputHandler, &SquishXmlOutputHandler::updateStatus, m_controlBar, &SquishControlBar::updateProgressText); } const QRect rect = Core::ICore::dialogParent()->screen()->availableGeometry(); m_controlBar->move(rect.width() - m_controlBar->width() - 10, 10); m_controlBar->showNormal(); } void SquishPerspective::destroyControlBar() { if (!m_controlBar) return; delete m_controlBar; m_controlBar = nullptr; } void SquishPerspective::resetAutId() { m_autIdKnown = false; m_inspectAction->setEnabled(false); } void SquishPerspective::setPerspectiveMode(PerspectiveMode mode) { if (m_mode == mode) // ignore return; m_mode = mode; switch (m_mode) { case Running: m_stopRecordAction->setEnabled(false); m_pausePlayAction->setEnabled(true); m_pausePlayAction->setIcon(iconForType(IconType::Pause)); m_pausePlayAction->setToolTip(Tr::tr("Interrupt")); m_stepInAction->setEnabled(false); m_stepOverAction->setEnabled(false); m_stepOutAction->setEnabled(false); m_stopAction->setEnabled(true); m_inspectAction->setEnabled(false); break; case Recording: m_stopRecordAction->setEnabled(true); m_pausePlayAction->setEnabled(false); m_pausePlayAction->setIcon(iconForType(IconType::Pause)); m_pausePlayAction->setToolTip(Tr::tr("Interrupt")); m_stepInAction->setEnabled(false); m_stepOverAction->setEnabled(false); m_stepOutAction->setEnabled(false); m_stopAction->setEnabled(true); m_inspectAction->setEnabled(false); break; case Interrupted: m_pausePlayAction->setEnabled(true); m_pausePlayAction->setIcon(iconForType(IconType::Play)); m_pausePlayAction->setToolTip(Tr::tr("Continue")); m_stepInAction->setEnabled(true); m_stepOverAction->setEnabled(true); m_stepOutAction->setEnabled(true); m_stopAction->setEnabled(true); m_inspectAction->setEnabled(m_autIdKnown); break; case Configuring: case Querying: case NoMode: m_pausePlayAction->setIcon(iconForType(IconType::Pause)); m_pausePlayAction->setToolTip(Tr::tr("Interrupt")); m_pausePlayAction->setEnabled(false); m_stopRecordAction->setEnabled(false); m_stepInAction->setEnabled(false); m_stepOverAction->setEnabled(false); m_stepOutAction->setEnabled(false); m_stopAction->setEnabled(false); m_inspectAction->setEnabled(false); m_localsModel.clear(); m_objectsModel.clear(); m_propertiesModel.clear(); break; default: break; } } } // namespace Internal } // namespace Squish