Files
qt-creator/src/plugins/autotest/testnavigationwidget.cpp
Christian Stenger b402215daf AutoTest: Cache expanded state as well
Parsing may end up with incomplete information when parsing documents
that are currently edited and contain syntax errors due to
incomplete changes.
This resulted in not re-adding all items which in turn led to lose
also its expanded state when re-adding it in a later successful parse.
Avoid the need to expand manually again and again by caching the
expanded state for a couple of re-parses.

Change-Id: I2994b9c7ab56c0c76f416d10247e68dea271cd10
Reviewed-by: David Schulz <david.schulz@qt.io>
2020-06-18 11:24:36 +00:00

363 lines
14 KiB
C++

/****************************************************************************
**
** Copyright (C) 2016 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 "testnavigationwidget.h"
#include "testframeworkmanager.h"
#include "testtreemodel.h"
#include "testtreeview.h"
#include "testtreeitemdelegate.h"
#include "testcodeparser.h"
#include "testrunner.h"
#include "autotestconstants.h"
#include "autotesticons.h"
#include "testtreeitem.h"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/find/itemviewfind.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/icore.h>
#include <texteditor/texteditor.h>
#include <utils/progressindicator.h>
#include <utils/utilsicons.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <projectexplorer/buildmanager.h>
#include <projectexplorer/session.h>
#include <QAction>
#include <QMenu>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
namespace Autotest {
namespace Internal {
TestNavigationWidget::TestNavigationWidget(QWidget *parent) :
QWidget(parent)
{
setWindowTitle(tr("Tests"));
m_model = TestTreeModel::instance();
m_sortFilterModel = new TestTreeSortFilterModel(m_model, m_model);
m_sortFilterModel->setDynamicSortFilter(true);
m_view = new TestTreeView(this);
m_view->setModel(m_sortFilterModel);
m_view->setSortingEnabled(true);
m_view->setItemDelegate(new TestTreeItemDelegate(this));
QPalette pal;
pal.setColor(QPalette::Window,
Utils::creatorTheme()->color(Utils::Theme::InfoBarBackground));
pal.setColor(QPalette::WindowText,
Utils::creatorTheme()->color(Utils::Theme::InfoBarText));
m_missingFrameworksWidget = new QFrame;
m_missingFrameworksWidget->setPalette(pal);
m_missingFrameworksWidget->setAutoFillBackground(true);
QHBoxLayout *hLayout = new QHBoxLayout;
m_missingFrameworksWidget->setLayout(hLayout);
hLayout->addWidget(new QLabel(tr("No active test frameworks.")));
const bool hasActiveFrameworks = Utils::anyOf(TestFrameworkManager::registeredFrameworks(),
&ITestFramework::active);
m_missingFrameworksWidget->setVisible(!hasActiveFrameworks);
QVBoxLayout *layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(m_missingFrameworksWidget);
layout->addWidget(Core::ItemViewFind::createSearchableWrapper(m_view));
setLayout(layout);
connect(m_view, &TestTreeView::activated, this, &TestNavigationWidget::onItemActivated);
m_progressIndicator = new Utils::ProgressIndicator(Utils::ProgressIndicatorSize::Medium, this);
m_progressIndicator->attachToWidget(m_view);
m_progressIndicator->hide();
m_progressTimer = new QTimer(this);
m_progressTimer->setSingleShot(true);
m_progressTimer->setInterval(1000); // don't display indicator if progress takes less than 1s
connect(m_model->parser(), &TestCodeParser::parsingStarted,
this, &TestNavigationWidget::onParsingStarted);
connect(m_model->parser(), &TestCodeParser::parsingFinished,
this, &TestNavigationWidget::onParsingFinished);
connect(m_model->parser(), &TestCodeParser::parsingFailed,
this, &TestNavigationWidget::onParsingFinished);
connect(m_model, &TestTreeModel::updatedActiveFrameworks,
this, [this] (int numberOfActive) {
m_missingFrameworksWidget->setVisible(numberOfActive == 0);
});
ProjectExplorer::SessionManager *sm = ProjectExplorer::SessionManager::instance();
connect(sm, &ProjectExplorer::SessionManager::startupProjectChanged,
[this](ProjectExplorer::Project * /*project*/) {
m_expandedStateCache.clear();
m_itemUseCache.clear();
});
connect(m_model, &TestTreeModel::testTreeModelChanged,
this, &TestNavigationWidget::reapplyCachedExpandedState);
connect(m_progressTimer, &QTimer::timeout,
m_progressIndicator, &Utils::ProgressIndicator::show);
}
void TestNavigationWidget::contextMenuEvent(QContextMenuEvent *event)
{
const bool enabled = !ProjectExplorer::BuildManager::isBuilding()
&& !TestRunner::instance()->isTestRunning()
&& m_model->parser()->state() == TestCodeParser::Idle;
QMenu menu;
QAction *runThisTest = nullptr;
QAction *runWithoutDeploy = nullptr;
QAction *debugThisTest = nullptr;
QAction *debugWithoutDeploy = nullptr;
const QModelIndexList list = m_view->selectionModel()->selectedIndexes();
if (list.size() == 1) {
const QModelIndex index = list.first();
QRect rect(m_view->visualRect(index));
if (rect.contains(event->pos())) {
TestTreeItem *item = static_cast<TestTreeItem *>(
m_model->itemForIndex(m_sortFilterModel->mapToSource(index)));
if (item->canProvideTestConfiguration()) {
runThisTest = new QAction(tr("Run This Test"), &menu);
runThisTest->setEnabled(enabled);
connect(runThisTest, &QAction::triggered,
this, [this] () {
onRunThisTestTriggered(TestRunMode::Run);
});
runWithoutDeploy = new QAction(tr("Run Without Deployment"), &menu);
runWithoutDeploy->setEnabled(enabled);
connect(runWithoutDeploy, &QAction::triggered,
this, [this] () {
onRunThisTestTriggered(TestRunMode::RunWithoutDeploy);
});
}
if (item->canProvideDebugConfiguration()) {
debugThisTest = new QAction(tr("Debug This Test"), &menu);
debugThisTest->setEnabled(enabled);
connect(debugThisTest, &QAction::triggered,
this, [this] () {
onRunThisTestTriggered(TestRunMode::Debug);
});
debugWithoutDeploy = new QAction(tr("Debug Without Deployment"), &menu);
debugWithoutDeploy->setEnabled(enabled);
connect(debugWithoutDeploy, &QAction::triggered,
this, [this] () {
onRunThisTestTriggered(TestRunMode::DebugWithoutDeploy);
});
}
}
}
QAction *runAll = Core::ActionManager::command(Constants::ACTION_RUN_ALL_ID)->action();
QAction *runSelected = Core::ActionManager::command(Constants::ACTION_RUN_SELECTED_ID)->action();
QAction *selectAll = new QAction(tr("Select All"), &menu);
QAction *deselectAll = new QAction(tr("Deselect All"), &menu);
// TODO remove?
QAction *rescan = Core::ActionManager::command(Constants::ACTION_SCAN_ID)->action();
connect(selectAll, &QAction::triggered, m_view, &TestTreeView::selectAll);
connect(deselectAll, &QAction::triggered, m_view, &TestTreeView::deselectAll);
if (runThisTest) {
menu.addAction(runThisTest);
menu.addAction(runWithoutDeploy);
}
if (debugThisTest) {
menu.addAction(debugThisTest);
menu.addAction(debugWithoutDeploy);
}
if (runThisTest || debugThisTest)
menu.addSeparator();
menu.addAction(runAll);
menu.addAction(runSelected);
menu.addSeparator();
menu.addAction(selectAll);
menu.addAction(deselectAll);
menu.addSeparator();
menu.addAction(rescan);
menu.exec(mapToGlobal(event->pos()));
}
QList<QToolButton *> TestNavigationWidget::createToolButtons()
{
QList<QToolButton *> list;
m_filterButton = new QToolButton(m_view);
m_filterButton->setIcon(Utils::Icons::FILTER.icon());
m_filterButton->setToolTip(tr("Filter Test Tree"));
m_filterButton->setProperty("noArrow", true);
m_filterButton->setPopupMode(QToolButton::InstantPopup);
m_filterMenu = new QMenu(m_filterButton);
initializeFilterMenu();
connect(m_filterMenu, &QMenu::triggered, this, &TestNavigationWidget::onFilterMenuTriggered);
m_filterButton->setMenu(m_filterMenu);
m_sortAlphabetically = true;
m_sort = new QToolButton(this);
m_sort->setIcon(Icons::SORT_NATURALLY.icon());
m_sort->setToolTip(tr("Sort Naturally"));
QToolButton *expand = new QToolButton(this);
expand->setIcon(Utils::Icons::EXPAND_TOOLBAR.icon());
expand->setToolTip(tr("Expand All"));
QToolButton *collapse = new QToolButton(this);
collapse->setIcon(Utils::Icons::COLLAPSE_TOOLBAR.icon());
collapse->setToolTip(tr("Collapse All"));
connect(expand, &QToolButton::clicked, m_view, &TestTreeView::expandAll);
connect(collapse, &QToolButton::clicked, m_view, &TestTreeView::collapseAll);
connect(m_sort, &QToolButton::clicked, this, &TestNavigationWidget::onSortClicked);
list << m_filterButton << m_sort << expand << collapse;
return list;
}
void TestNavigationWidget::updateExpandedStateCache()
{
// raise generation for cached items and drop anything reaching 10th generation
const QList<QString> cachedNames = m_itemUseCache.keys();
for (const QString &cachedName : cachedNames) {
auto it = m_itemUseCache.find(cachedName);
if (it.value()++ >= 10) {
m_itemUseCache.erase(it);
m_expandedStateCache.remove(cachedName);
}
}
for (Utils::TreeItem *rootNode : *m_model->rootItem()) {
rootNode->forAllChildren([this](Utils::TreeItem *child) {
auto childItem = static_cast<TestTreeItem *>(child);
const QString cacheName = childItem->cacheName();
m_expandedStateCache.insert(cacheName, m_view->isExpanded(childItem->index()));
m_itemUseCache[cacheName] = 0; // explicitly mark as 0-generation
});
}
}
void TestNavigationWidget::onItemActivated(const QModelIndex &index)
{
const Utils::Link link = index.data(LinkRole).value<Utils::Link>();
if (link.hasValidTarget()) {
Core::EditorManager::openEditorAt(link.targetFileName, link.targetLine,
link.targetColumn);
}
}
void TestNavigationWidget::onSortClicked()
{
if (m_sortAlphabetically) {
m_sort->setIcon(Utils::Icons::SORT_ALPHABETICALLY_TOOLBAR.icon());
m_sort->setToolTip(tr("Sort Alphabetically"));
m_sortFilterModel->setSortMode(TestTreeItem::Naturally);
} else {
m_sort->setIcon(Icons::SORT_NATURALLY.icon());
m_sort->setToolTip(tr("Sort Naturally"));
m_sortFilterModel->setSortMode(TestTreeItem::Alphabetically);
}
m_sortAlphabetically = !m_sortAlphabetically;
}
void TestNavigationWidget::onFilterMenuTriggered(QAction *action)
{
m_sortFilterModel->toggleFilter(
TestTreeSortFilterModel::toFilterMode(action->data().value<int>()));
}
void TestNavigationWidget::onParsingStarted()
{
m_progressTimer->start();
}
void TestNavigationWidget::onParsingFinished()
{
m_progressTimer->stop();
m_progressIndicator->hide();
}
void TestNavigationWidget::initializeFilterMenu()
{
QAction *action = new QAction(m_filterMenu);
action->setText(tr("Show Init and Cleanup Functions"));
action->setCheckable(true);
action->setChecked(false);
action->setData(TestTreeSortFilterModel::ShowInitAndCleanup);
m_filterMenu->addAction(action);
action = new QAction(m_filterMenu);
action->setText(tr("Show Data Functions"));
action->setCheckable(true);
action->setChecked(false);
action->setData(TestTreeSortFilterModel::ShowTestData);
m_filterMenu->addAction(action);
}
void TestNavigationWidget::onRunThisTestTriggered(TestRunMode runMode)
{
const QModelIndexList selected = m_view->selectionModel()->selectedIndexes();
if (selected.isEmpty())
return;
const QModelIndex &sourceIndex = m_sortFilterModel->mapToSource(selected.first());
if (!sourceIndex.isValid())
return;
TestTreeItem *item = static_cast<TestTreeItem *>(sourceIndex.internalPointer());
TestRunner::instance()->runTest(runMode, item);
}
void TestNavigationWidget::reapplyCachedExpandedState()
{
for (Utils::TreeItem *rootNode : *m_model->rootItem()) {
rootNode->forAllChildren([this](Utils::TreeItem *child) {
auto childItem = static_cast<TestTreeItem *>(child);
const QString cacheName = childItem->cacheName();
const auto it = m_expandedStateCache.find(cacheName);
if (it == m_expandedStateCache.end())
return;
QModelIndex index = child->index();
if (m_view->isExpanded(index) != it.value())
m_view->setExpanded(index, it.value());
});
}
}
TestNavigationWidgetFactory::TestNavigationWidgetFactory()
{
setDisplayName(tr("Tests"));
setId(Autotest::Constants::AUTOTEST_ID);
setPriority(666);
}
Core::NavigationView TestNavigationWidgetFactory::createWidget()
{
TestNavigationWidget *treeViewWidget = new TestNavigationWidget;
Core::NavigationView view;
view.widget = treeViewWidget;
view.dockToolBarWidgets = treeViewWidget->createToolButtons();
return view;
}
} // namespace Internal
} // namespace Autotest