Provide Squish integration plugin

Change-Id: I43ae2ad35441339fa48714c14094b8003d0cf7c3
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Christian Stenger
2022-06-30 16:04:45 +02:00
parent 1257a4c5c3
commit ce74e9fb2d
62 changed files with 7980 additions and 1 deletions

View File

@@ -82,6 +82,7 @@ add_subdirectory(valgrind)
add_subdirectory(perfprofiler)
add_subdirectory(qbsprojectmanager)
add_subdirectory(ctfvisualizer)
add_subdirectory(squish)
# Level 8:
add_subdirectory(boot2qt)

View File

@@ -73,6 +73,7 @@ Project {
"scxmleditor/scxmleditor.qbs",
"serialterminal/serialterminal.qbs",
"silversearcher/silversearcher.qbs",
"squish/squish.qbs",
"studiowelcome/studiowelcome.qbs",
"subversion/subversion.qbs",
"texteditor/texteditor.qbs",

View File

@@ -0,0 +1,29 @@
add_qtc_plugin(Squish
PLUGIN_DEPENDS
Core
DEPENDS ExtensionSystem Utils
SOURCES
deletesymbolicnamedialog.cpp deletesymbolicnamedialog.h deletesymbolicnamedialog.ui
objectsmapdocument.cpp objectsmapdocument.h
objectsmapeditor.cpp objectsmapeditor.h
objectsmapeditorwidget.cpp objectsmapeditorwidget.h
objectsmaptreeitem.cpp objectsmaptreeitem.h
opensquishsuitesdialog.cpp opensquishsuitesdialog.h opensquishsuitesdialog.ui
propertyitemdelegate.cpp propertyitemdelegate.h
propertytreeitem.cpp propertytreeitem.h
squish.qrc
squishfilehandler.cpp squishfilehandler.h
squishnavigationwidget.cpp squishnavigationwidget.h
squishoutputpane.cpp squishoutputpane.h
squishplugin.cpp squishplugin.h
squishresultmodel.cpp squishresultmodel.h
squishsettings.cpp squishsettings.h
squishsettingspage.cpp squishsettingspage.h squishsettingspage.ui
squishtesttreemodel.cpp squishtesttreemodel.h
squishtesttreeview.cpp squishtesttreeview.h
squishtools.cpp squishtools.h
squishutils.cpp squishutils.h
squishxmloutputhandler.cpp squishxmloutputhandler.h
symbolnameitemdelegate.cpp symbolnameitemdelegate.h
testresult.cpp testresult.h
)

View File

@@ -0,0 +1,30 @@
{
\"Name\" : \"Squish\",
\"Version\" : \"$$QTCREATOR_VERSION\",
\"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
\"Experimental\" : true,
\"Vendor\" : \"The Qt Company Ltd\",
\"Copyright\" : \"(C) 2022 The Qt Company Ltd\",
\"License\" : [ \"Commercial Usage\",
\"\",
\"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt 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.\",
\"\",
\"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.\"
],
\"Description\" : \"Squish plugin. Provides integration of Squish.\",
\"Url\" : \"http://www.qt.io\",
$$dependencyList,
\"Mimetypes\" : [
\"<?xml version=\'1.0\'?>\",
\"<mime-info xmlns=\'http://www.freedesktop.org/standards/shared-mime-info\'>\",
\" <mime-type type=\'text/squish-objectsmap\'>\",
\" <sub-class-of type=\'text/plain\'/>\",
\" <comment>Squish objects.map File</comment>\",
\" <glob pattern=\'objects.map\'/>\",
\" </mime-type>\",
\"</mime-info>\"
]
}

View File

@@ -0,0 +1,122 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "deletesymbolicnamedialog.h"
#include "ui_deletesymbolicnamedialog.h"
#include <QItemSelection>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QStringListModel>
namespace Squish {
namespace Internal {
DeleteSymbolicNameDialog::DeleteSymbolicNameDialog(const QString &symbolicName,
const QStringList &names,
QWidget *parent)
: QDialog(parent)
, ui(new Ui::DeleteSymbolicNameDialog)
, m_result(ResetReference)
{
ui->setupUi(this);
ui->filterLineEdit->setFiltering(true);
m_listModel = new QStringListModel(this);
m_filterModel = new QSortFilterProxyModel(this);
m_filterModel->setSourceModel(m_listModel);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
ui->symbolicNamesList->setModel(m_filterModel);
updateDetailsLabel(symbolicName);
populateSymbolicNamesList(names);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(ui->adjustReferencesRB,
&QRadioButton::toggled,
this,
&DeleteSymbolicNameDialog::onAdjustReferencesToggled);
connect(ui->removeAndInvalidateRB, &QRadioButton::toggled, this, [this](bool checked) {
if (checked)
m_result = InvalidateNames;
});
connect(ui->removeAllRB, &QRadioButton::toggled, this, [this](bool checked) {
if (checked)
m_result = RemoveNames;
});
connect(ui->symbolicNamesList->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&DeleteSymbolicNameDialog::onSelectionChanged);
connect(ui->filterLineEdit,
&Utils::FancyLineEdit::filterChanged,
m_filterModel,
&QSortFilterProxyModel::setFilterFixedString);
}
DeleteSymbolicNameDialog::~DeleteSymbolicNameDialog()
{
delete ui;
}
void DeleteSymbolicNameDialog::updateDetailsLabel(const QString &nameToDelete)
{
const char *detailsText = QT_TR_NOOP(
"The Symbolic Name <span style='white-space: nowrap'>\"%1\"</span> you "
"want to remove is used in Multi Property Names. Please decide what to do "
"with the references in these Multi Property Names.");
ui->detailsLabel->setText(tr(detailsText).arg(nameToDelete));
}
void DeleteSymbolicNameDialog::populateSymbolicNamesList(const QStringList &symbolicNames)
{
m_listModel->setStringList(symbolicNames);
m_filterModel->sort(0);
}
void DeleteSymbolicNameDialog::onAdjustReferencesToggled(bool checked)
{
ui->symbolicNamesList->setEnabled(checked);
const bool enable = !checked || ui->symbolicNamesList->selectionModel()->hasSelection();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable);
if (checked)
m_result = ResetReference;
}
void DeleteSymbolicNameDialog::onSelectionChanged(const QItemSelection &selection,
const QItemSelection &)
{
const bool empty = selection.isEmpty();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!empty);
if (empty)
m_selected.clear();
else
m_selected = selection.indexes().first().data().toString();
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,69 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QDialog>
QT_BEGIN_NAMESPACE
class QItemSelection;
class QSortFilterProxyModel;
class QStringListModel;
QT_END_NAMESPACE
namespace Ui { class DeleteSymbolicNameDialog; }
namespace Squish {
namespace Internal {
class DeleteSymbolicNameDialog : public QDialog
{
Q_OBJECT
public:
enum Result { ResetReference, InvalidateNames, RemoveNames };
explicit DeleteSymbolicNameDialog(const QString &symbolicName,
const QStringList &names,
QWidget *parent = nullptr);
~DeleteSymbolicNameDialog() override;
QString selectedSymbolicName() const { return m_selected; }
Result result() const { return m_result; }
private:
void updateDetailsLabel(const QString &nameToDelete);
void populateSymbolicNamesList(const QStringList &symbolicNames);
void onAdjustReferencesToggled(bool checked);
void onSelectionChanged(const QItemSelection &selection, const QItemSelection &);
Ui::DeleteSymbolicNameDialog *ui;
QString m_selected;
Result m_result;
QStringListModel *m_listModel;
QSortFilterProxyModel *m_filterModel;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeleteSymbolicNameDialog</class>
<widget class="QDialog" name="DeleteSymbolicNameDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>395</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="detailsLabel">
<property name="text">
<string>Details</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="adjustReferencesRB">
<property name="text">
<string>Adjust references to the removed symbolic name to point to:</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Utils::FancyLineEdit" name="filterLineEdit"/>
</item>
<item>
<widget class="QListView" name="symbolicNamesList"/>
</item>
<item>
<widget class="QRadioButton" name="removeAndInvalidateRB">
<property name="text">
<string>Remove the symbolic name (invalidates names referencing it)</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="removeAllRB">
<property name="text">
<string>Remove the symbolic name and all names referencing it</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Utils::FancyLineEdit</class>
<extends>QLineEdit</extends>
<header location="global">utils/fancylineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>DeleteSymbolicNameDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>DeleteSymbolicNameDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

View File

@@ -0,0 +1,227 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "objectsmapdocument.h"
#include "objectsmaptreeitem.h"
#include "squishconstants.h"
#include <utils/fileutils.h>
#include <QDir>
namespace Squish {
namespace Internal {
static const char kItemSeparator = '\n';
static const char kPropertySeparator = '\t';
ObjectsMapDocument::ObjectsMapDocument()
: m_contentModel(new ObjectsMapModel(this))
, m_isModified(false)
{
setMimeType(Constants::SQUISH_OBJECTSMAP_MIMETYPE);
setId(Constants::OBJECTSMAP_EDITOR_ID);
connect(m_contentModel, &ObjectsMapModel::modelChanged, this, [this]() { setModified(true); });
}
Core::IDocument::OpenResult ObjectsMapDocument::open(QString *errorString,
const Utils::FilePath &fileName,
const Utils::FilePath &realFileName)
{
OpenResult result = openImpl(errorString, fileName, realFileName);
if (result == OpenResult::Success) {
setFilePath(fileName);
setModified(fileName != realFileName);
}
return result;
}
bool ObjectsMapDocument::save(QString *errorString, const Utils::FilePath &fileName, bool autoSave)
{
const Utils::FilePath actual = fileName.isEmpty() ? filePath() : fileName;
if (actual.isEmpty())
return false;
const bool writeOk = writeFile(actual);
if (!writeOk) {
if (errorString)
*errorString = tr("Failed to write \"%1\"").arg(actual.toUserOutput());
return false;
}
if (!autoSave) {
setModified(false);
setFilePath(actual);
}
return true;
}
Utils::FilePath ObjectsMapDocument::fallbackSaveAsPath() const
{
return Utils::FilePath();
}
QString ObjectsMapDocument::fallbackSaveAsFileName() const
{
return "objects.map";
}
void ObjectsMapDocument::setModified(bool modified)
{
m_isModified = modified;
emit changed();
}
bool ObjectsMapDocument::reload(QString *errorString,
Core::IDocument::ReloadFlag flag,
Core::IDocument::ChangeType type)
{
Q_UNUSED(type);
if (flag == FlagIgnore)
return true;
emit aboutToReload();
const bool success = (openImpl(errorString, filePath(), filePath()) == OpenResult::Success);
if (success)
setModified(false);
emit reloadFinished(success);
return success;
}
bool ObjectsMapDocument::buildObjectsMapTree(const QByteArray &contents)
{
QMap<QString, ObjectsMapTreeItem *> itemForName;
// get names and their properties as we don't have correct (creation) order inside objects.map
const QList<QByteArray> lines = contents.split(kItemSeparator);
for (const QByteArray &line : lines) {
if (line.isEmpty())
continue;
const int tabPosition = line.indexOf(kPropertySeparator);
const QString objectName = QLatin1String(line.left(tabPosition).trimmed());
if (!objectName.startsWith(ObjectsMapTreeItem::COLON)) {
qDeleteAll(itemForName);
return false;
}
ObjectsMapTreeItem *item = new ObjectsMapTreeItem(objectName,
Qt::ItemIsEnabled | Qt::ItemIsSelectable
| Qt::ItemIsEditable);
item->setPropertiesContent(line.mid(tabPosition + 1).trimmed());
itemForName.insert(objectName, item);
item->initPropertyModelConnections(m_contentModel);
}
// now build the tree
ObjectsMapTreeItem *root = new ObjectsMapTreeItem(QString());
QMap<QString, ObjectsMapTreeItem *>::iterator end = itemForName.end();
for (ObjectsMapTreeItem *item : qAsConst(itemForName)) {
const QString &parentName = item->parentName();
auto parent = itemForName.find(parentName);
if (parent != end)
parent.value()->appendChild(item);
else
root->appendChild(item);
}
m_contentModel->changeRootItem(root);
return true;
}
bool ObjectsMapDocument::setContents(const QByteArray &contents)
{
return buildObjectsMapTree(contents);
}
QByteArray ObjectsMapDocument::contents() const
{
QByteArray result;
QMap<QString, PropertyList> objects;
m_contentModel->forAllItems([&objects](ObjectsMapTreeItem *item) {
if (item->parent())
objects.insert(item->data(0, Qt::DisplayRole).toString(), item->properties());
});
const QStringList &keys = objects.keys();
for (const QString &objName : keys) {
result.append(objName.toUtf8());
result.append(kPropertySeparator);
const PropertyList properties = objects.value(objName);
// ensure to store invalid properties content as is instead of an empty {}
if (properties.isEmpty()) {
if (Utils::TreeItem *item = m_contentModel->findItem(objName)) {
ObjectsMapTreeItem *objMapItem = static_cast<ObjectsMapTreeItem *>(item);
if (!objMapItem->isValid()) {
result.append(objMapItem->propertiesContent()).append(kItemSeparator);
continue;
}
}
}
result.append('{');
for (const Property &property : properties) {
result.append(property.toString().toUtf8());
result.append(' ');
}
// remove the last space added by the last property
if (result.at(result.size() - 1) == ' ')
result.chop(1);
result.append('}');
result.append(kItemSeparator);
}
return result;
}
Core::IDocument::OpenResult ObjectsMapDocument::openImpl(QString *error,
const Utils::FilePath &fileName,
const Utils::FilePath &realFileName)
{
if (fileName.isEmpty())
return OpenResult::CannotHandle;
Utils::FileReader reader;
if (!reader.fetch(realFileName, QIODevice::Text, error))
return OpenResult::ReadError;
const QString text = QString::fromLocal8Bit(reader.data());
if (!setContents(text.toUtf8())) {
if (error)
error->append(tr("Failure while parsing objects.map content."));
return OpenResult::ReadError;
}
return OpenResult::Success;
}
bool ObjectsMapDocument::writeFile(const Utils::FilePath &fileName) const
{
Utils::FileSaver saver(fileName);
return saver.write(contents()) && saver.finalize();
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,72 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <coreplugin/idocument.h>
#include <QList>
namespace Squish {
namespace Internal {
class ObjectsMapModel;
class ObjectsMapDocument : public Core::IDocument
{
Q_OBJECT
public:
ObjectsMapDocument();
OpenResult open(QString *errorString,
const Utils::FilePath &fileName,
const Utils::FilePath &realFileName) override;
bool save(QString *errorString, const Utils::FilePath &fileName, bool autoSave) override;
Utils::FilePath fallbackSaveAsPath() const override;
QString fallbackSaveAsFileName() const override;
bool isModified() const override { return m_isModified; }
void setModified(bool modified);
bool isSaveAsAllowed() const override { return true; }
bool reload(QString *errorString, ReloadFlag flag, ChangeType type) override;
bool shouldAutoSave() const override { return true; }
bool setContents(const QByteArray &contents) override;
QByteArray contents() const override;
ObjectsMapModel *model() const { return m_contentModel; }
private:
OpenResult openImpl(QString *error,
const Utils::FilePath &fileName,
const Utils::FilePath &realFileName);
bool buildObjectsMapTree(const QByteArray &contents);
bool writeFile(const Utils::FilePath &fileName) const;
void syncXMLFromEditor();
ObjectsMapModel *m_contentModel;
bool m_isModified;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,74 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "objectsmapeditor.h"
#include "objectsmapdocument.h"
#include "objectsmapeditorwidget.h"
#include "squishconstants.h"
namespace Squish {
namespace Internal {
ObjectsMapEditor::ObjectsMapEditor(QSharedPointer<ObjectsMapDocument> document)
: m_document(document)
{
setWidget(new ObjectsMapEditorWidget(m_document.data()));
setDuplicateSupported(true);
}
ObjectsMapEditor::~ObjectsMapEditor()
{
delete m_widget;
}
Core::IDocument *ObjectsMapEditor::document() const
{
return m_document.data();
}
QWidget *ObjectsMapEditor::toolBar()
{
return nullptr;
}
Core::IEditor *ObjectsMapEditor::duplicate()
{
return new ObjectsMapEditor(m_document);
}
/********************************* EditorFactory ********************************************/
ObjectsMapEditorFactory::ObjectsMapEditorFactory()
{
setId(Constants::OBJECTSMAP_EDITOR_ID);
setDisplayName("Squish Object Map Editor");
addMimeType(Constants::SQUISH_OBJECTSMAP_MIMETYPE);
setEditorCreator([]() {
return new ObjectsMapEditor(QSharedPointer<ObjectsMapDocument>(new ObjectsMapDocument));
});
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,63 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <coreplugin/editormanager/ieditor.h>
#include <coreplugin/editormanager/ieditorfactory.h>
#include <QSharedPointer>
namespace Squish {
namespace Internal {
class ObjectsMapDocument;
class ObjectsMapEditorWidget;
class ObjectsMapTreeItem;
class ObjectsMapEditor : public Core::IEditor
{
Q_OBJECT
public:
ObjectsMapEditor(QSharedPointer<ObjectsMapDocument> document);
~ObjectsMapEditor() override;
Core::IDocument *document() const override;
QWidget *toolBar() override;
Core::IEditor *duplicate() override;
private:
QSharedPointer<ObjectsMapDocument> m_document;
};
class ObjectsMapEditorFactory : public Core::IEditorFactory
{
Q_OBJECT
public:
ObjectsMapEditorFactory();
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,705 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "objectsmapeditorwidget.h"
#include "deletesymbolicnamedialog.h"
#include "objectsmapdocument.h"
#include "objectsmaptreeitem.h"
#include "propertyitemdelegate.h"
#include "symbolnameitemdelegate.h"
#include <coreplugin/icore.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <QApplication>
#include <QClipboard>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QItemSelection>
#include <QItemSelectionModel>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QMimeData>
#include <QModelIndex>
#include <QPushButton>
#include <QRegularExpression>
#include <QStackedLayout>
#include <QTreeView>
namespace Squish {
namespace Internal {
static const char objectsMapObjectMimeType[] = "application/vnd.qtcreator.objectsmapobject";
static const char objectsMapPropertyMimeType[] = "application/vnd.qtcreator.objectsmapproperty";
ObjectsMapEditorWidget::ObjectsMapEditorWidget(ObjectsMapDocument *document, QWidget *parent)
: QWidget(parent)
, m_document(document)
{
initUi();
initializeConnections();
initializeContextMenus();
}
void ObjectsMapEditorWidget::initUi()
{
setGeometry(0, 0, 550, 585);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(new QLabel(tr("<b>Symbolic Names</b>"), this));
m_filterLineEdit = new Utils::FancyLineEdit(this);
m_filterLineEdit->setFiltering(true);
mainLayout->addWidget(m_filterLineEdit);
QHBoxLayout *horizontalLayout = new QHBoxLayout;
m_symbolicNamesTreeView = new QTreeView(this);
horizontalLayout->addWidget(m_symbolicNamesTreeView);
QVBoxLayout *verticalLayout = new QVBoxLayout;
m_newSymbolicName = new QPushButton(tr("New"));
verticalLayout->addWidget(m_newSymbolicName);
m_removeSymbolicName = new QPushButton(tr("Remove"));
m_removeSymbolicName->setEnabled(false);
verticalLayout->addWidget(m_removeSymbolicName);
verticalLayout->addSpacerItem(
new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
horizontalLayout->addLayout(verticalLayout);
m_propertiesLabel = new QLabel(this);
m_propertiesLabel->setWordWrap(true);
mainLayout->addLayout(horizontalLayout);
mainLayout->addWidget(m_propertiesLabel);
m_stackedLayout = new QStackedLayout;
QWidget *validPropertiesWidget = new QWidget(this);
QHBoxLayout *horizontalLayout2 = new QHBoxLayout;
m_propertiesTree = new QTreeView(this);
m_propertiesTree->setIndentation(20);
m_propertiesTree->setRootIsDecorated(false);
m_propertiesTree->setUniformRowHeights(true);
m_propertiesTree->setItemsExpandable(false);
m_propertiesTree->setExpandsOnDoubleClick(false);
horizontalLayout2->addWidget(m_propertiesTree);
QVBoxLayout *verticalLayout2 = new QVBoxLayout;
m_newProperty = new QPushButton(tr("New"), this);
m_newProperty->setEnabled(false);
verticalLayout2->addWidget(m_newProperty);
m_removeProperty = new QPushButton(tr("Remove"), this);
m_removeProperty->setEnabled(false);
verticalLayout2->addWidget(m_removeProperty);
m_jumpToSymbolicName = new QPushButton(this);
m_jumpToSymbolicName->setEnabled(false);
m_jumpToSymbolicName->setIcon(QIcon(":/squish/images/jumpTo.png"));
m_jumpToSymbolicName->setToolTip(tr("Jump to Symbolic Name"));
verticalLayout2->addWidget(m_jumpToSymbolicName);
verticalLayout2->addSpacerItem(
new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
horizontalLayout2->addLayout(verticalLayout2);
validPropertiesWidget->setLayout(horizontalLayout2);
m_stackedLayout->addWidget(validPropertiesWidget);
QWidget *invalidPropertiesWidget = new QWidget(this);
QVBoxLayout *verticalLayout3 = new QVBoxLayout;
m_propertiesLineEdit = new QLineEdit(this);
verticalLayout3->addWidget(m_propertiesLineEdit);
verticalLayout3->addSpacerItem(
new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
invalidPropertiesWidget->setLayout(verticalLayout3);
m_stackedLayout->addWidget(invalidPropertiesWidget);
mainLayout->addLayout(m_stackedLayout);
setLayout(mainLayout);
m_objMapFilterModel = new ObjectsMapSortFilterModel(m_document->model(), this);
m_objMapFilterModel->setDynamicSortFilter(true);
m_symbolicNamesTreeView->setModel(m_objMapFilterModel);
m_symbolicNamesTreeView->setSortingEnabled(true);
m_symbolicNamesTreeView->setHeaderHidden(true);
SymbolNameItemDelegate *symbolDelegate = new SymbolNameItemDelegate(this);
m_symbolicNamesTreeView->setItemDelegate(symbolDelegate);
m_symbolicNamesTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
PropertyItemDelegate *propertyDelegate = new PropertyItemDelegate(this);
m_propertiesTree->setItemDelegate(propertyDelegate);
m_propertiesTree->setContextMenuPolicy(Qt::CustomContextMenu);
m_propertiesSortModel = new PropertiesSortModel(this);
m_propertiesTree->setModel(m_propertiesSortModel);
m_propertiesTree->setSortingEnabled(true);
m_propertiesTree->header()->setSortIndicatorShown(false);
m_propertiesTree->header()->setSectionsClickable(false);
m_propertiesTree->header()->setSectionsMovable(false);
setPropertiesDisplayValid(true);
}
void ObjectsMapEditorWidget::initializeConnections()
{
connect(m_filterLineEdit,
&Utils::FancyLineEdit::filterChanged,
this,
[this](const QString &filter) {
m_objMapFilterModel->setFilterFixedString(filter);
QItemSelectionModel *selectionModel = m_symbolicNamesTreeView->selectionModel();
if (selectionModel->hasSelection())
m_symbolicNamesTreeView->scrollTo(selectionModel->selectedIndexes().first());
});
connect(m_document->model(),
&ObjectsMapModel::requestSelection,
this,
&ObjectsMapEditorWidget::onSelectionRequested);
connect(m_symbolicNamesTreeView->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&ObjectsMapEditorWidget::onObjectSelectionChanged);
connect(m_jumpToSymbolicName,
&QPushButton::clicked,
this,
&ObjectsMapEditorWidget::onJumpToSymbolicNameClicked);
connect(m_symbolicNamesTreeView,
&QTreeView::customContextMenuRequested,
this,
[this](const QPoint &pos) {
m_symbolicNamesCtxtMenu->exec(m_symbolicNamesTreeView->mapToGlobal(pos));
});
connect(m_propertiesTree,
&QTreeView::customContextMenuRequested,
this,
[this](const QPoint &pos) {
m_propertiesCtxtMenu->exec(m_propertiesTree->mapToGlobal(pos));
});
connect(m_propertiesLineEdit,
&QLineEdit::textChanged,
this,
&ObjectsMapEditorWidget::onPropertiesContentModified);
connect(m_newProperty,
&QPushButton::clicked,
this,
&ObjectsMapEditorWidget::onNewPropertyTriggered);
connect(m_removeProperty,
&QPushButton::clicked,
this,
&ObjectsMapEditorWidget::onRemovePropertyTriggered);
connect(m_newSymbolicName,
&QPushButton::clicked,
this,
&ObjectsMapEditorWidget::onNewSymbolicNameTriggered);
connect(m_removeSymbolicName,
&QPushButton::clicked,
this,
&ObjectsMapEditorWidget::onRemoveSymbolicNameTriggered);
}
void ObjectsMapEditorWidget::initializeContextMenus()
{
m_symbolicNamesCtxtMenu = new QMenu(m_symbolicNamesTreeView);
QAction *cutAction = new QAction(tr("Cut"), m_symbolicNamesCtxtMenu);
cutAction->setShortcut(QKeySequence(QKeySequence::Cut));
connect(cutAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onCutSymbolicNameTriggered);
QAction *copyAction = new QAction(tr("Copy"), m_symbolicNamesCtxtMenu);
copyAction->setShortcut(QKeySequence(QKeySequence::Copy));
connect(copyAction, &QAction::triggered, this, &ObjectsMapEditorWidget::onCopySymbolTriggered);
QAction *pasteAction = new QAction(tr("Paste"), m_symbolicNamesCtxtMenu);
pasteAction->setShortcut(QKeySequence(QKeySequence::Paste));
connect(pasteAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onPasteSymbolicNameTriggered);
QAction *deleteAction = new QAction(tr("Delete"), m_symbolicNamesCtxtMenu);
deleteAction->setShortcut(QKeySequence(QKeySequence::Delete));
connect(deleteAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onRemoveSymbolicNameTriggered);
QAction *copyRealNameAction = new QAction(tr("Copy Real Name"), m_symbolicNamesCtxtMenu);
connect(copyRealNameAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onCopyRealNameTriggered);
m_symbolicNamesCtxtMenu->addAction(cutAction);
m_symbolicNamesCtxtMenu->addAction(copyAction);
m_symbolicNamesCtxtMenu->addAction(pasteAction);
m_symbolicNamesCtxtMenu->addAction(deleteAction);
m_symbolicNamesCtxtMenu->addAction(copyRealNameAction);
m_propertiesCtxtMenu = new QMenu(m_propertiesTree);
cutAction = new QAction(tr("Cut"), m_propertiesCtxtMenu);
cutAction->setShortcut(QKeySequence(QKeySequence::Cut));
connect(cutAction, &QAction::triggered, this, &ObjectsMapEditorWidget::onCutPropertyTriggered);
copyAction = new QAction(tr("Copy"), m_propertiesCtxtMenu);
copyAction->setShortcut(QKeySequence(QKeySequence::Copy));
connect(copyAction, &QAction::triggered, this, &ObjectsMapEditorWidget::onCopyPropertyTriggered);
pasteAction = new QAction(tr("Paste"), m_propertiesCtxtMenu);
pasteAction->setShortcut(QKeySequence(QKeySequence::Paste));
connect(pasteAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onPastePropertyTriggered);
deleteAction = new QAction(tr("Delete"), m_propertiesCtxtMenu);
deleteAction->setShortcut(QKeySequence(QKeySequence::Delete));
connect(deleteAction,
&QAction::triggered,
this,
&ObjectsMapEditorWidget::onRemovePropertyTriggered);
m_propertiesCtxtMenu->addAction(cutAction);
m_propertiesCtxtMenu->addAction(copyAction);
m_propertiesCtxtMenu->addAction(pasteAction);
m_propertiesCtxtMenu->addAction(deleteAction);
}
void ObjectsMapEditorWidget::setPropertiesDisplayValid(bool valid)
{
static const char *propertiesValidText = QT_TR_NOOP(
"<b>Properties:</b><br/>"
"The properties of the Multi Property Name associated with the selected "
"Symbolic Name. (use \\\\ for a literal \\ in the value)");
static const char *propertiesInvalidText = QT_TR_NOOP(
"<b>Properties:</b><br/>"
"The Hierarchical Name associated with the selected Symbolic Name.");
m_propertiesLabel->setText(tr(valid ? propertiesValidText : propertiesInvalidText));
m_stackedLayout->setCurrentIndex(valid ? 0 : 1);
}
void ObjectsMapEditorWidget::onSelectionRequested(const QModelIndex &idx)
{
QItemSelectionModel *selectionModel = m_symbolicNamesTreeView->selectionModel();
selectionModel->select(m_objMapFilterModel->mapFromSource(idx),
QItemSelectionModel::ClearAndSelect);
m_symbolicNamesTreeView->scrollTo(selectionModel->selectedIndexes().first());
}
void ObjectsMapEditorWidget::onObjectSelectionChanged(const QItemSelection &selected,
const QItemSelection & /*deselected*/)
{
QModelIndexList modelIndexes = selected.indexes();
if (modelIndexes.isEmpty()) {
m_propertiesTree->setModel(nullptr);
m_removeSymbolicName->setEnabled(false);
m_jumpToSymbolicName->setEnabled(false);
m_newProperty->setEnabled(false);
m_removeProperty->setEnabled(false);
return;
}
const QModelIndex &idx = m_objMapFilterModel->mapToSource(modelIndexes.first());
if (auto item = static_cast<ObjectsMapTreeItem *>(m_document->model()->itemForIndex(idx))) {
const bool valid = item->isValid();
if (valid) {
m_propertiesSortModel->setSourceModel(item->propertiesModel());
m_propertiesTree->setModel(m_propertiesSortModel);
connect(m_propertiesTree->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&ObjectsMapEditorWidget::onPropertySelectionChanged,
Qt::UniqueConnection);
m_newProperty->setEnabled(true);
m_removeSymbolicName->setEnabled(true);
m_jumpToSymbolicName->setEnabled(false);
m_removeProperty->setEnabled(false);
} else {
m_propertiesLineEdit->setText(QLatin1String(item->propertiesContent()));
m_propertiesLineEdit->setCursorPosition(0);
}
setPropertiesDisplayValid(valid);
}
}
void ObjectsMapEditorWidget::onPropertySelectionChanged(const QItemSelection &selected,
const QItemSelection & /*deselected*/)
{
QModelIndexList modelIndexes = selected.indexes();
if (modelIndexes.isEmpty()) {
m_jumpToSymbolicName->setEnabled(false);
m_removeProperty->setEnabled(false);
} else {
const QModelIndex current = modelIndexes.first();
if (current.isValid()) {
m_removeProperty->setEnabled(true);
const QString is = current.sibling(current.row(), 1).data().toString();
m_jumpToSymbolicName->setEnabled(is == Property::OPERATOR_IS);
}
}
}
void ObjectsMapEditorWidget::onPropertiesContentModified(const QString &text)
{
if (!m_propertiesLineEdit->isModified())
return;
const QModelIndexList selected = m_symbolicNamesTreeView->selectionModel()->selectedIndexes();
if (selected.isEmpty())
return;
const QModelIndex &idx = m_objMapFilterModel->mapToSource(selected.first());
if (auto item = static_cast<ObjectsMapTreeItem *>(m_document->model()->itemForIndex(idx)))
item->setPropertiesContent(text.toLocal8Bit().trimmed());
}
void ObjectsMapEditorWidget::onJumpToSymbolicNameClicked()
{
QModelIndexList selectedIndexes = m_propertiesTree->selectionModel()->selectedIndexes();
if (selectedIndexes.isEmpty())
return;
if (auto model = qobject_cast<PropertiesModel *>(m_propertiesSortModel->sourceModel())) {
const QModelIndex propIdx = m_propertiesSortModel->mapToSource(selectedIndexes.first());
Utils::TreeItem *item = model->itemForIndex(propIdx);
const QString symbolicName = item->data(2, Qt::DisplayRole).toString();
if (Utils::TreeItem *found = m_document->model()->findItem(symbolicName)) {
const QModelIndex idx = m_document->model()->indexForItem(found);
QItemSelectionModel *selectionModel = m_symbolicNamesTreeView->selectionModel();
selectionModel->select(m_objMapFilterModel->mapFromSource(idx),
QItemSelectionModel::ClearAndSelect);
m_symbolicNamesTreeView->scrollTo(m_objMapFilterModel->mapFromSource(idx));
}
}
}
static QString generateName(const QStringList &names, const QString &nameTmpl, int start = 1)
{
int value;
for (value = start; names.contains(nameTmpl + QString::number(value)); ++value)
;
return nameTmpl + QString::number(value);
}
void ObjectsMapEditorWidget::onNewPropertyTriggered()
{
static QString nameTemplate = "New";
PropertiesModel *propertiesModel = qobject_cast<PropertiesModel *>(
m_propertiesSortModel->sourceModel());
Utils::TreeItem *root = propertiesModel->rootItem();
QStringList propertyNames;
propertyNames.reserve(root->childCount());
root->forChildrenAtLevel(1, [&propertyNames](Utils::TreeItem *child) {
propertyNames.append(static_cast<PropertyTreeItem *>(child)->property().m_name);
});
Property property;
property.m_name = generateName(propertyNames, nameTemplate);
PropertyTreeItem *propertyItem = new PropertyTreeItem(property);
m_propertiesTree->clearSelection();
propertiesModel->addNewProperty(propertyItem);
const QModelIndex srcIdx = propertiesModel->indexForItem(propertyItem);
m_propertiesTree->edit(m_propertiesSortModel->mapFromSource(srcIdx));
}
void ObjectsMapEditorWidget::onRemovePropertyTriggered()
{
if (PropertyTreeItem *item = selectedPropertyItem()) {
auto model = qobject_cast<PropertiesModel *>(m_propertiesSortModel->sourceModel());
model->removeProperty(item);
}
}
void ObjectsMapEditorWidget::onNewSymbolicNameTriggered()
{
static QString nameTemplate = ":NewName";
ObjectsMapModel *objMapModel = qobject_cast<ObjectsMapModel *>(
m_objMapFilterModel->sourceModel());
const QStringList objNames = objMapModel->allSymbolicNames();
ObjectsMapTreeItem *objMapItem = new ObjectsMapTreeItem(generateName(objNames, nameTemplate),
Qt::ItemIsEnabled | Qt::ItemIsSelectable
| Qt::ItemIsEditable);
objMapItem->initPropertyModelConnections(m_document->model());
m_symbolicNamesTreeView->clearSelection();
objMapModel->addNewObject(objMapItem);
const QModelIndex idx = m_objMapFilterModel->mapFromSource(
objMapModel->indexForItem(objMapItem));
m_symbolicNamesTreeView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
// make sure PropertiesTree is updated as well
onObjectSelectionChanged(QItemSelection(idx, idx), QItemSelection());
m_symbolicNamesTreeView->edit(idx);
}
void ObjectsMapEditorWidget::onRemoveSymbolicNameTriggered()
{
ObjectsMapModel *objMapModel = qobject_cast<ObjectsMapModel *>(
m_objMapFilterModel->sourceModel());
const QModelIndexList &selected = m_symbolicNamesTreeView->selectionModel()->selectedIndexes();
QTC_ASSERT(!selected.isEmpty(), return );
const QModelIndex idx = selected.first();
const QString symbolicName = idx.data().toString();
// if symbol has children it is the window or container for these
bool hasReference = m_objMapFilterModel->hasChildren(idx);
QMap<QString, PropertyList> objects;
objMapModel->forAllItems([&objects](ObjectsMapTreeItem *item) {
if (item->parent())
objects.insert(item->data(0, Qt::DisplayRole).toString(), item->properties());
});
hasReference |= Utils::anyOf(objects, [&symbolicName](const PropertyList &props) {
return Utils::anyOf(props, [&symbolicName](const Property &p) {
return p.m_value == symbolicName && p.isRelativeWidget();
});
});
DeleteSymbolicNameDialog::Result result = DeleteSymbolicNameDialog::RemoveNames;
QString newReference;
if (hasReference) {
DeleteSymbolicNameDialog dialog(symbolicName, objects.keys(), Core::ICore::dialogParent());
if (dialog.exec() != QDialog::Accepted)
return;
result = dialog.result();
newReference = dialog.selectedSymbolicName();
} else {
// Squish does not ask for removing objects without references, but we prefer to do it
if (QMessageBox::question(Core::ICore::dialogParent(),
tr("Remove Symbolic Name"),
tr("Do you really want to remove \"%1\"?").arg(symbolicName))
!= QMessageBox::Yes)
return;
}
switch (result) {
case DeleteSymbolicNameDialog::ResetReference:
objMapModel->removeSymbolicNameResetReferences(symbolicName, newReference);
break;
case DeleteSymbolicNameDialog::InvalidateNames:
objMapModel->removeSymbolicNameInvalidateReferences(m_objMapFilterModel->mapToSource(idx));
break;
case DeleteSymbolicNameDialog::RemoveNames:
objMapModel->removeSymbolicName(m_objMapFilterModel->mapToSource(idx));
break;
}
}
void ObjectsMapEditorWidget::onCopySymbolTriggered()
{
ObjectsMapTreeItem *item = selectedObjectItem();
if (!item)
return;
const QModelIndex idx = m_document->model()->indexForItem(item);
const QString &symbolicName = idx.data().toString();
QMimeData *data = new QMimeData;
data->setText(symbolicName);
data->setData(objectsMapObjectMimeType, item->propertiesToByteArray());
QApplication::clipboard()->setMimeData(data);
}
void ObjectsMapEditorWidget::onPasteSymbolicNameTriggered()
{
const QMimeData *data = QApplication::clipboard()->mimeData();
if (!data)
return;
QString symbolicName = data->text();
if (symbolicName.isEmpty())
return;
if (symbolicName.at(0) != ObjectsMapTreeItem::COLON)
symbolicName.prepend(ObjectsMapTreeItem::COLON);
else if (symbolicName.size() == 1)
return;
// if name is not valid at all refuse to do anything
const QRegularExpression validName("^:[^\t\n\r\f\b\v\a]+$");
if (!validName.match(symbolicName).hasMatch())
return;
if (auto objMapModel = qobject_cast<ObjectsMapModel *>(m_objMapFilterModel->sourceModel())) {
QStringList usedSymbolicNames = objMapModel->allSymbolicNames();
// check if name is valid and if not, try to get a new one
if (usedSymbolicNames.contains(symbolicName))
symbolicName = ambiguousNameDialog(symbolicName, usedSymbolicNames, false);
if (symbolicName.isEmpty())
return;
ObjectsMapTreeItem *objMapItem = new ObjectsMapTreeItem(symbolicName,
Qt::ItemIsEnabled
| Qt::ItemIsSelectable
| Qt::ItemIsEditable);
// if it's our mime data insert a symbolic name including properties
if (data->hasFormat(objectsMapObjectMimeType)) {
QByteArray properties = data->data(objectsMapObjectMimeType);
if (!properties.isEmpty())
objMapItem->setPropertiesContent(properties);
}
objMapItem->initPropertyModelConnections(m_document->model());
objMapModel->addNewObject(objMapItem);
const QModelIndex idx = m_objMapFilterModel->mapFromSource(
objMapModel->indexForItem(objMapItem));
m_symbolicNamesTreeView->scrollTo(idx, QAbstractItemView::PositionAtCenter);
m_symbolicNamesTreeView->selectionModel()->select(QItemSelection(idx, idx),
QItemSelectionModel::ClearAndSelect);
}
}
void ObjectsMapEditorWidget::onCopyRealNameTriggered()
{
if (ObjectsMapTreeItem *item = selectedObjectItem())
QApplication::clipboard()->setText(QLatin1String(item->propertiesToByteArray()));
}
void ObjectsMapEditorWidget::onCutSymbolicNameTriggered()
{
onCopySymbolTriggered();
onRemoveSymbolicNameTriggered();
}
void ObjectsMapEditorWidget::onCopyPropertyTriggered()
{
PropertyTreeItem *item = selectedPropertyItem();
if (!item)
return;
QMimeData *data = new QMimeData;
data->setText(item->property().toString());
data->setData(objectsMapPropertyMimeType, item->property().toString().toUtf8());
QApplication::clipboard()->setMimeData(data);
}
void ObjectsMapEditorWidget::onCutPropertyTriggered()
{
onCopyPropertyTriggered();
onRemovePropertyTriggered();
}
void ObjectsMapEditorWidget::onPastePropertyTriggered()
{
const QMimeData *data = QApplication::clipboard()->mimeData();
// we only handle our own copied mime data
if (!data || !data->hasFormat(objectsMapPropertyMimeType))
return;
if (auto sortModel = qobject_cast<PropertiesSortModel *>(m_propertiesTree->model())) {
Property property = Property(data->data(objectsMapPropertyMimeType));
if (property.m_name.isEmpty())
return;
auto propertiesModel = qobject_cast<PropertiesModel *>(sortModel->sourceModel());
const QStringList &usedProperties = propertiesModel->allPropertyNames();
if (usedProperties.contains(property.m_name)) {
property.m_name = ambiguousNameDialog(property.m_name, usedProperties, true);
if (property.m_name.isEmpty())
return;
}
PropertyTreeItem *propertyItem = new PropertyTreeItem(property);
propertiesModel->addNewProperty(propertyItem);
}
}
QString ObjectsMapEditorWidget::ambiguousNameDialog(const QString &original,
const QStringList &usedNames,
bool isProperty)
{
QTC_ASSERT(!original.isEmpty(), return QString());
QDialog dialog(this);
dialog.setModal(true);
dialog.setWindowTitle(isProperty ? tr("Ambiguous Property Name")
: tr("Ambiguous Symbolic Name"));
QVBoxLayout *layout = new QVBoxLayout;
QLabel label(tr("%1 \"%2\" already exists. Specify a unique name.")
.arg(isProperty ? tr("Property") : tr("Symbolic Name"))
.arg(original));
layout->addWidget(&label);
Utils::FancyLineEdit *validator;
if (isProperty)
validator = new ValidatingPropertyNameLineEdit(usedNames, &dialog);
else
validator = new ValidatingContainerNameLineEdit(usedNames, &dialog);
layout->addWidget(validator);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
| QDialogButtonBox::Cancel,
&dialog);
layout->addWidget(buttonBox);
connect(validator,
&ValidatingPropertyNameLineEdit::validChanged,
buttonBox->button(QDialogButtonBox::Ok),
&QPushButton::setEnabled);
connect(buttonBox->button(QDialogButtonBox::Ok),
&QPushButton::clicked,
&dialog,
&QDialog::accept);
connect(buttonBox->button(QDialogButtonBox::Cancel),
&QPushButton::clicked,
&dialog,
&QDialog::reject);
QString validName(original);
if (isProperty) {
validName[0] = validName[0].toUpper();
validName = tr("CopyOf") + validName;
}
// make sure the name is unique
if (usedNames.contains(validName))
validName = generateName(usedNames, validName, 2);
validator->setText(validName);
dialog.setLayout(layout);
if (dialog.exec() == QDialog::Accepted && validator->isValid())
return validator->text();
return QString();
}
ObjectsMapTreeItem *ObjectsMapEditorWidget::selectedObjectItem() const
{
const QModelIndexList &selected = m_symbolicNamesTreeView->selectionModel()->selectedIndexes();
QTC_ASSERT(!selected.isEmpty(), return nullptr);
if (auto proxyModel = qobject_cast<QSortFilterProxyModel *>(m_symbolicNamesTreeView->model())) {
if (auto model = qobject_cast<ObjectsMapModel *>(proxyModel->sourceModel())) {
const QModelIndex idx = m_objMapFilterModel->mapToSource(selected.first());
return static_cast<ObjectsMapTreeItem *>(model->itemForIndex(idx));
}
}
return nullptr;
}
PropertyTreeItem *ObjectsMapEditorWidget::selectedPropertyItem() const
{
auto propertiesModel = qobject_cast<PropertiesModel *>(m_propertiesSortModel->sourceModel());
const QModelIndexList &selectedIndexes = m_propertiesTree->selectionModel()->selectedIndexes();
QTC_ASSERT(!selectedIndexes.isEmpty(), return nullptr);
const QModelIndex idx = m_propertiesSortModel->mapToSource(selectedIndexes.first());
return static_cast<PropertyTreeItem *>(propertiesModel->itemForIndex(idx));
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,107 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QWidget>
QT_BEGIN_NAMESPACE
class QItemSelection;
class QLabel;
class QLineEdit;
class QMenu;
class QPushButton;
class QStackedLayout;
class QTreeView;
QT_END_NAMESPACE
namespace Utils {
class FancyLineEdit;
}
namespace Squish {
namespace Internal {
class ObjectsMapDocument;
class ObjectsMapSortFilterModel;
class ObjectsMapTreeItem;
class PropertiesSortModel;
class PropertyTreeItem;
class ObjectsMapEditorWidget : public QWidget
{
Q_OBJECT
public:
explicit ObjectsMapEditorWidget(ObjectsMapDocument *document, QWidget *parent = nullptr);
private:
void initUi();
void initializeConnections();
void initializeContextMenus();
void setPropertiesDisplayValid(bool valid);
void onSelectionRequested(const QModelIndex &idx);
void onObjectSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
void onPropertySelectionChanged(const QItemSelection &selected,
const QItemSelection &deselected);
void onPropertiesContentModified(const QString &text);
void onJumpToSymbolicNameClicked();
void onNewPropertyTriggered();
void onRemovePropertyTriggered();
void onNewSymbolicNameTriggered();
void onRemoveSymbolicNameTriggered();
void onCopySymbolTriggered();
void onPasteSymbolicNameTriggered();
void onCopyRealNameTriggered();
void onCutSymbolicNameTriggered();
void onCopyPropertyTriggered();
void onCutPropertyTriggered();
void onPastePropertyTriggered();
QString ambiguousNameDialog(const QString &original,
const QStringList &usedNames,
bool isProperty);
ObjectsMapTreeItem *selectedObjectItem() const;
PropertyTreeItem *selectedPropertyItem() const;
ObjectsMapDocument *m_document;
ObjectsMapSortFilterModel *m_objMapFilterModel;
PropertiesSortModel *m_propertiesSortModel;
QMenu *m_symbolicNamesCtxtMenu;
QMenu *m_propertiesCtxtMenu;
Utils::FancyLineEdit *m_filterLineEdit;
QTreeView *m_symbolicNamesTreeView;
QTreeView *m_propertiesTree;
QPushButton *m_newSymbolicName;
QPushButton *m_removeSymbolicName;
QPushButton *m_newProperty;
QPushButton *m_removeProperty;
QPushButton *m_jumpToSymbolicName;
QLineEdit *m_propertiesLineEdit;
QLabel *m_propertiesLabel;
QStackedLayout *m_stackedLayout;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,493 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "objectsmaptreeitem.h"
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <QDebug>
using namespace Utils;
namespace Squish {
namespace Internal {
const QChar ObjectsMapTreeItem::COLON = ':';
/************************* ObjectsMapTreeItem ***********************************************/
ObjectsMapTreeItem::ObjectsMapTreeItem(const QString &name, Qt::ItemFlags flags)
: m_propertiesModel(new PropertiesModel(this))
, m_name(name)
, m_flags(flags)
{}
ObjectsMapTreeItem::~ObjectsMapTreeItem()
{
delete m_propertiesModel;
}
// these connections cannot be setup from inside the ctor as we have the need to be already
// child of a model, so this must be setup after the item has been added to a model
// (or as soon the model is available)
void ObjectsMapTreeItem::initPropertyModelConnections(ObjectsMapModel *objMapModel)
{
QObject::connect(m_propertiesModel,
&PropertiesModel::propertyChanged,
objMapModel,
&ObjectsMapModel::propertyChanged);
QObject::connect(m_propertiesModel,
&PropertiesModel::propertyRemoved,
objMapModel,
&ObjectsMapModel::propertyRemoved);
QObject::connect(m_propertiesModel,
&PropertiesModel::propertyAdded,
objMapModel,
&ObjectsMapModel::propertyAdded);
}
QVariant ObjectsMapTreeItem::data(int column, int role) const
{
if (column == 0 && role == Qt::DisplayRole)
return m_name;
return TreeItem::data(column, role);
}
bool ObjectsMapTreeItem::setData(int column, const QVariant &data, int role)
{
if (column != 0 || role != Qt::EditRole)
return false;
m_name = data.toString();
return true;
}
Qt::ItemFlags ObjectsMapTreeItem::flags(int /*column*/) const
{
return m_flags;
}
void ObjectsMapTreeItem::setPropertiesContent(const QByteArray &content)
{
if (parseProperties(content)) {
m_propertiesContent.clear();
return;
}
m_propertiesContent = content;
}
QByteArray ObjectsMapTreeItem::propertiesToByteArray() const
{
if (!isValid())
return propertiesContent();
QByteArray result;
PropertyList properties = this->properties();
sort(properties,
[](const Property &lhs, const Property &rhs) { return lhs.m_name < rhs.m_name; });
result.append('{');
for (const Property &property : qAsConst(properties))
result.append(property.toString().toUtf8()).append(' ');
if (result.at(result.size() - 1) == ' ')
result.chop(1);
result.append('}');
return result;
}
QString ObjectsMapTreeItem::parentName() const
{
QString result;
if (auto propertyItem = m_propertiesModel->findItemAtLevel<1>([](TreeItem *item) {
return static_cast<PropertyTreeItem *>(item)->property().isContainer();
})) {
result = propertyItem->data(2, Qt::DisplayRole).toString();
}
return result;
}
PropertyList ObjectsMapTreeItem::properties() const
{
PropertyList result;
m_propertiesModel->forItemsAtLevel<1>([&result](Utils::TreeItem *item) {
result.append(static_cast<PropertyTreeItem *>(item)->property());
});
return result;
}
bool ObjectsMapTreeItem::parseProperties(const QByteArray &properties)
{
enum ParseState { None, Name, Operator, Value };
TreeItem *propertyRoot = m_propertiesModel->rootItem();
QTC_ASSERT(propertyRoot, return false);
// if we perform a re-parse, we might have already children
propertyRoot->removeChildren();
if (properties.isEmpty() || properties.at(0) != '{')
return false;
ParseState state = None;
QByteArray name;
QByteArray value;
QByteArray oper;
bool masquerading = false;
for (char c : properties) {
if (masquerading) {
value.append('\\').append(c);
masquerading = false;
continue;
}
switch (c) {
case '=':
if (state == Value) {
value.append(c);
} else if ((state == Name) || (state == Operator && oper.size() < 2)) {
state = Operator;
oper.append(c);
} else {
propertyRoot->removeChildren();
return false;
}
break;
case '?':
case '~':
if (state == Name || (state == Operator && oper.isEmpty())) {
state = Operator;
oper.append(c);
} else if (state == Value) {
value.append(c);
} else {
propertyRoot->removeChildren();
return false;
}
break;
case '\'':
if (state == Operator) {
state = Value;
} else if (state == Value) {
state = None;
Property prop;
if (!prop.set(QLatin1String(name), QLatin1String(oper), QLatin1String(value))) {
propertyRoot->removeChildren();
return false;
}
m_propertiesModel->addNewProperty(new PropertyTreeItem(prop));
name.clear();
oper.clear();
value.clear();
} else {
propertyRoot->removeChildren();
return false;
}
break;
case '{':
if (state == None) {
state = Name;
} else if (state == Value) {
value.append(c);
} else {
propertyRoot->removeChildren();
return false;
}
break;
case '}':
if (state == Value) {
value.append(c);
} else if (state == None) {
return true;
} else {
propertyRoot->removeChildren();
return false;
}
break;
case '\\':
if (state == Value) {
masquerading = true;
} else {
propertyRoot->removeChildren();
return false;
}
break;
default:
if (isspace(c)) {
if (state == Value) {
value.append(c);
} else if (state == Name) {
state = Operator;
} else if (state == Operator) {
if (!oper.endsWith('=')) {
propertyRoot->removeChildren();
return false;
}
}
} else {
if (state == None) {
state = Name;
name.append(c);
} else if (state == Name) {
name.append(c);
} else if (state == Value) {
value.append(c);
} else {
propertyRoot->removeChildren();
return false;
}
}
}
}
if (masquerading || state != None)
propertyRoot->removeChildren();
return state == None;
}
/******************************* ObjectsMapModel ********************************************/
ObjectsMapModel::ObjectsMapModel(QObject *parent)
: TreeModel<ObjectsMapTreeItem>(new ObjectsMapTreeItem(""), parent)
{
connect(this, &ObjectsMapModel::propertyChanged, this, &ObjectsMapModel::onPropertyChanged);
connect(this, &ObjectsMapModel::propertyRemoved, this, &ObjectsMapModel::onPropertyRemoved);
connect(this, &ObjectsMapModel::nameChanged, this, &ObjectsMapModel::onNameChanged);
connect(this, &ObjectsMapModel::propertyAdded, this, &ObjectsMapModel::modelChanged);
}
bool ObjectsMapModel::setData(const QModelIndex &idx, const QVariant &data, int role)
{
// only allow editing here
if (role != Qt::EditRole)
return false;
const QString old = idx.data().toString();
QString modified = data.toString();
if (modified.isEmpty())
return false;
if (modified.at(0) != ObjectsMapTreeItem::COLON)
modified.prepend(ObjectsMapTreeItem::COLON);
bool result = TreeModel::setData(idx, modified, role);
if (result) {
emit nameChanged(old, modified);
emit requestSelection(idx);
}
return result;
}
void ObjectsMapModel::addNewObject(ObjectsMapTreeItem *item)
{
QTC_ASSERT(item, return );
QTC_ASSERT(rootItem(), return );
rootItem()->appendChild(item);
emit modelChanged();
}
ObjectsMapTreeItem *ObjectsMapModel::findItem(const QString &search) const
{
return findNonRootItem(
[search](ObjectsMapTreeItem *item) { return item->data(0, Qt::DisplayRole) == search; });
}
void ObjectsMapModel::removeSymbolicNameResetReferences(const QString &symbolicName,
const QString &newRef)
{
ObjectsMapTreeItem *item = findItem(symbolicName);
QTC_ASSERT(item, return );
forAllItems([&symbolicName, &newRef](ObjectsMapTreeItem *item) {
// ignore invisible root and invalid item
if (!item->parent() || !item->isValid())
return;
PropertiesModel *propertiesModel = item->propertiesModel();
propertiesModel->modifySpecialProperty(symbolicName, newRef);
return;
});
delete takeItem(item);
emit modelChanged();
}
void ObjectsMapModel::removeSymbolicNameInvalidateReferences(const QModelIndex &idx)
{
TreeItem *item = itemForIndex(idx);
QTC_ASSERT(item, return );
item->forAllChildren([this](TreeItem *childItem) {
ObjectsMapTreeItem *objMapItem = static_cast<ObjectsMapTreeItem *>(childItem);
takeItem(objMapItem);
addNewObject(objMapItem);
});
delete takeItem(item);
emit modelChanged();
}
void ObjectsMapModel::removeSymbolicName(const QModelIndex &idx)
{
TreeItem *item = itemForIndex(idx);
QTC_ASSERT(item, return );
delete takeItem(item);
emit modelChanged();
}
QStringList ObjectsMapModel::allSymbolicNames() const
{
TreeItem *root = rootItem();
QTC_ASSERT(root, return QStringList());
QMap<QString, PropertyList> objects;
forAllItems([&objects](ObjectsMapTreeItem *item) {
if (item->parent())
objects.insert(item->data(0, Qt::DisplayRole).toString(), item->properties());
});
return objects.keys();
}
void ObjectsMapModel::changeRootItem(ObjectsMapTreeItem *newRoot)
{
setRootItem(newRoot);
}
void ObjectsMapModel::onNameChanged(const QString &old, const QString &modified)
{
if (old != modified) {
// walk over all ObjectsMapTreeItems
QTC_ASSERT(rootItem(), return );
forSelectedItems([&old, &modified](ObjectsMapTreeItem *item) {
if (!item->parent())
return true;
PropertiesModel *pm = item->propertiesModel();
// walk over properties of this object
QTC_ASSERT(pm->rootItem(), return true);
pm->forAllItems([&old, &modified](PropertyTreeItem *propItem) {
const Property &prop = propItem->property();
if ((prop.isContainer() || prop.isRelativeWidget()) && prop.m_value == old)
propItem->setData(2, modified, Qt::EditRole);
});
return true;
});
emit modelChanged();
}
}
void ObjectsMapModel::onPropertyChanged(
ObjectsMapTreeItem *item, const QString &old, const QString &modified, int row, int column)
{
QTC_ASSERT(item, return );
if (old == modified)
return;
// special handling for changes of container properties
if (column == 2 || column == 0) {
PropertiesModel *propModel = item->propertiesModel();
const QModelIndex propIndex = propModel->index(row, column, QModelIndex());
auto propertyItem = static_cast<PropertyTreeItem *>(propModel->itemForIndex(propIndex));
Property property = propertyItem->property();
if (property.isContainer()) {
takeItem(item);
ObjectsMapTreeItem *foundItem = findItem(property.m_value);
QTC_ASSERT(foundItem, return ); // could not find new parent should not happen
foundItem->appendChild(item);
emit requestSelection(indexForItem(item));
emit modelChanged();
}
}
}
void ObjectsMapModel::onPropertyRemoved(ObjectsMapTreeItem *item, const Property &property)
{
QTC_ASSERT(item, return );
if (property.isContainer()) {
takeItem(item);
QTC_ASSERT(rootItem(), return );
rootItem()->appendChild(item);
emit requestSelection(indexForItem(item));
emit modelChanged();
}
}
/***************************** SortFilterModel **********************************************/
ObjectsMapSortFilterModel::ObjectsMapSortFilterModel(TreeModel<ObjectsMapTreeItem> *sourceModel,
QObject *parent)
: QSortFilterProxyModel(parent)
{
setSourceModel(sourceModel);
}
bool ObjectsMapSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
return left.data().toString() > right.data().toString();
}
static bool checkRecursivelyForPattern(const QModelIndex &index,
TreeModel<> *model,
const QString &pattern)
{
if (index.data().toString().contains(pattern, Qt::CaseInsensitive))
return true;
// if no match - check if its properties...
ObjectsMapTreeItem *item = static_cast<ObjectsMapTreeItem *>(model->itemForIndex(index));
if (item && anyOf(item->properties(), [&pattern](const Property &p) {
return p.m_value.contains(pattern, Qt::CaseInsensitive);
})) {
return true;
}
// ...or a child might have a match
for (int row = 0, childCount = model->rowCount(index); row < childCount; ++row) {
const QModelIndex child = model->index(row, 0, index);
if (checkRecursivelyForPattern(child, model, pattern))
return true;
}
return false;
}
bool ObjectsMapSortFilterModel::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
const QString pattern = filterRegularExpression().pattern();
if (pattern.isEmpty())
return true;
TreeModel<> *srcModel = static_cast<TreeModel<> *>(sourceModel());
const QModelIndex index = srcModel->index(sourceRow, 0, sourceParent);
if (!index.isValid())
return false;
return checkRecursivelyForPattern(index, srcModel, pattern);
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,113 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "propertytreeitem.h"
#include <utils/treemodel.h>
#include <QSortFilterProxyModel>
namespace Squish {
namespace Internal {
class ObjectsMapModel;
class ObjectsMapTreeItem : public Utils::TreeItem
{
public:
explicit ObjectsMapTreeItem(const QString &name, Qt::ItemFlags flags = Qt::ItemIsEnabled);
~ObjectsMapTreeItem() override;
void initPropertyModelConnections(ObjectsMapModel *objMapModel);
QVariant data(int column, int role) const override;
bool setData(int column, const QVariant &data, int role) override;
Qt::ItemFlags flags(int column) const override;
bool isValid() const { return m_propertiesContent.isEmpty(); }
void setPropertiesContent(const QByteArray &content);
QByteArray propertiesContent() const { return m_propertiesContent; }
QByteArray propertiesToByteArray() const;
QString parentName() const;
PropertyList properties() const;
PropertiesModel *propertiesModel() const { return m_propertiesModel; }
static const QChar COLON;
private:
bool parseProperties(const QByteArray &properties);
PropertiesModel *m_propertiesModel;
QString m_name;
QByteArray m_propertiesContent; // for invalid properties content
Qt::ItemFlags m_flags = Qt::NoItemFlags;
};
class ObjectsMapModel : public Utils::TreeModel<ObjectsMapTreeItem>
{
Q_OBJECT
public:
ObjectsMapModel(QObject *parent = nullptr);
bool setData(const QModelIndex &idx, const QVariant &data, int role) override;
void addNewObject(ObjectsMapTreeItem *item);
ObjectsMapTreeItem *findItem(const QString &search) const;
void removeSymbolicNameResetReferences(const QString &symbolicName, const QString &newRef);
void removeSymbolicNameInvalidateReferences(const QModelIndex &idx);
void removeSymbolicName(const QModelIndex &idx);
QStringList allSymbolicNames() const;
void changeRootItem(ObjectsMapTreeItem *newRoot);
signals:
void requestSelection(const QModelIndex &idx);
void modelChanged();
void nameChanged(const QString &old, const QString &modified);
void propertyChanged(
ObjectsMapTreeItem *item, const QString &old, const QString &modified, int row, int column);
void propertyRemoved(ObjectsMapTreeItem *item, const Property &property);
void propertyAdded(ObjectsMapTreeItem *item);
private:
void onNameChanged(const QString &old, const QString &modified);
void onPropertyChanged(
ObjectsMapTreeItem *item, const QString &old, const QString &modified, int row, int column);
void onPropertyRemoved(ObjectsMapTreeItem *item, const Property &property);
};
class ObjectsMapSortFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
ObjectsMapSortFilterModel(Utils::TreeModel<ObjectsMapTreeItem> *sourceModel,
QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,132 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "opensquishsuitesdialog.h"
#include "squishutils.h"
#include "ui_opensquishsuitesdialog.h"
#include <QDir>
#include <QListWidgetItem>
#include <QPushButton>
namespace Squish {
namespace Internal {
static QString previousPath;
OpenSquishSuitesDialog::OpenSquishSuitesDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::OpenSquishSuitesDialog)
{
ui->setupUi(this);
ui->buttonBox->button(QDialogButtonBox::Open)->setEnabled(false);
connect(ui->directoryLineEdit,
&Utils::PathChooser::pathChanged,
this,
&OpenSquishSuitesDialog::onDirectoryChanged);
connect(ui->selectAllPushButton,
&QPushButton::clicked,
this,
&OpenSquishSuitesDialog::selectAll);
connect(ui->deselectAllPushButton,
&QPushButton::clicked,
this,
&OpenSquishSuitesDialog::deselectAll);
connect(this, &OpenSquishSuitesDialog::accepted, this, &OpenSquishSuitesDialog::setChosenSuites);
ui->directoryLineEdit->setPath(previousPath);
}
OpenSquishSuitesDialog::~OpenSquishSuitesDialog()
{
delete ui;
}
void OpenSquishSuitesDialog::onDirectoryChanged()
{
ui->suitesListWidget->clear();
ui->buttonBox->button(QDialogButtonBox::Open)->setEnabled(false);
QDir baseDir(ui->directoryLineEdit->path());
if (!baseDir.exists()) {
return;
}
const QFileInfoList subDirs = baseDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo &subDir : subDirs) {
if (!subDir.baseName().startsWith("suite_"))
continue;
if (SquishUtils::validTestCases(subDir.absoluteFilePath()).size()) {
QListWidgetItem *item = new QListWidgetItem(subDir.baseName(), ui->suitesListWidget);
item->setCheckState(Qt::Checked);
connect(ui->suitesListWidget,
&QListWidget::itemChanged,
this,
&OpenSquishSuitesDialog::onListItemChanged);
}
}
ui->buttonBox->button(QDialogButtonBox::Open)->setEnabled(ui->suitesListWidget->count());
}
void OpenSquishSuitesDialog::onListItemChanged(QListWidgetItem *)
{
const int count = ui->suitesListWidget->count();
for (int row = 0; row < count; ++row) {
if (ui->suitesListWidget->item(row)->checkState() == Qt::Checked) {
ui->buttonBox->button(QDialogButtonBox::Open)->setEnabled(true);
return;
}
}
ui->buttonBox->button(QDialogButtonBox::Open)->setEnabled(false);
}
void OpenSquishSuitesDialog::selectAll()
{
const int count = ui->suitesListWidget->count();
for (int row = 0; row < count; ++row)
ui->suitesListWidget->item(row)->setCheckState(Qt::Checked);
}
void OpenSquishSuitesDialog::deselectAll()
{
const int count = ui->suitesListWidget->count();
for (int row = 0; row < count; ++row)
ui->suitesListWidget->item(row)->setCheckState(Qt::Unchecked);
}
void OpenSquishSuitesDialog::setChosenSuites()
{
const int count = ui->suitesListWidget->count();
previousPath = ui->directoryLineEdit->path();
const QDir baseDir(previousPath);
for (int row = 0; row < count; ++row) {
QListWidgetItem *item = ui->suitesListWidget->item(row);
if (item->checkState() == Qt::Checked)
m_chosenSuites.append(QFileInfo(baseDir, item->text()).absoluteFilePath());
}
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,58 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QDialog>
QT_BEGIN_NAMESPACE
class QListWidgetItem;
QT_END_NAMESPACE
namespace Ui { class OpenSquishSuitesDialog; }
namespace Squish {
namespace Internal {
class OpenSquishSuitesDialog : public QDialog
{
Q_OBJECT
public:
explicit OpenSquishSuitesDialog(QWidget *parent = nullptr);
~OpenSquishSuitesDialog() override;
QStringList chosenSuites() const { return m_chosenSuites; }
private:
void onDirectoryChanged();
void onListItemChanged(QListWidgetItem *);
void selectAll();
void deselectAll();
void setChosenSuites();
Ui::OpenSquishSuitesDialog *ui;
QStringList m_chosenSuites;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OpenSquishSuitesDialog</class>
<widget class="QDialog" name="OpenSquishSuitesDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>303</width>
<height>340</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Open Squish Test Suites</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Base directory:</string>
</property>
</widget>
</item>
<item>
<widget class="Utils::PathChooser" name="directoryLineEdit"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Test suites:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QListWidget" name="suitesListWidget"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="selectAllPushButton">
<property name="text">
<string>Select All</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deselectAllPushButton">
<property name="text">
<string>Deselect All</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Open</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Utils::PathChooser</class>
<extends>QLineEdit</extends>
<header location="global">utils/pathchooser.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OpenSquishSuitesDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OpenSquishSuitesDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,183 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "propertyitemdelegate.h"
#include "objectsmaptreeitem.h"
#include <utils/treemodel.h>
#include <QComboBox>
#include <QCompleter>
#include <QLineEdit>
#include <QMouseEvent>
#include <QRegularExpression>
namespace Squish {
namespace Internal {
enum ViewColumn { Name, Operator, Value };
PropertyItemDelegate::PropertyItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{}
void PropertyItemDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
// paint invalid values red
if (index.column() == Value) {
if (auto sortModel = qobject_cast<const PropertiesSortModel *>(index.model())) {
if (auto propertiesModel = qobject_cast<PropertiesModel *>(sortModel->sourceModel())) {
const QModelIndex idx = sortModel->mapToSource(index);
PropertyTreeItem *item = static_cast<PropertyTreeItem *>(
propertiesModel->itemForIndex(idx));
const Property &property = item->property();
if (property.isContainer() || property.isRelativeWidget()) {
const ObjectsMapTreeItem *parent = propertiesModel->parentItem();
if (parent) {
if (const ObjectsMapModel *objMapModel
= qobject_cast<const ObjectsMapModel *>(parent->model())) {
if (!objMapModel->findItem(item->property().m_value))
opt.palette.setColor(QPalette::Text, QColor(0xff, 0, 0));
}
}
}
}
}
}
QStyledItemDelegate::paint(painter, opt, index);
}
QWidget *PropertyItemDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
switch (index.column()) {
case Name: {
auto sortModel = qobject_cast<const PropertiesSortModel *>(index.model());
PropertiesModel *pm = qobject_cast<PropertiesModel *>(sortModel->sourceModel());
Utils::TreeItem *self = pm->itemForIndex(sortModel->mapToSource(index));
QStringList forbidden;
pm->forItemsAtLevel<1>([&self, &forbidden](Utils::TreeItem *it) {
auto item = static_cast<PropertyTreeItem *>(it);
if (item != self)
forbidden.append(item->property().m_name);
});
return new ValidatingPropertyNameLineEdit(forbidden, parent);
}
case Operator: {
if (index.data().toString() == Property::OPERATOR_IS)
return nullptr;
QComboBox *comboBox = new QComboBox(parent);
comboBox->addItem(Property::OPERATOR_EQUALS);
comboBox->addItem(Property::OPERATOR_WILDCARD);
comboBox->addItem(Property::OPERATOR_REGEX);
comboBox->setFocusPolicy(Qt::StrongFocus);
comboBox->setAutoFillBackground(true);
return comboBox;
}
case Value: {
auto sortModel = qobject_cast<const PropertiesSortModel *>(index.model());
PropertiesModel *pm = qobject_cast<PropertiesModel *>(sortModel->sourceModel());
PropertyTreeItem *self = static_cast<PropertyTreeItem *>(
pm->itemForIndex(sortModel->mapToSource(index)));
if (self->property().isContainer() || self->property().isRelativeWidget()) {
auto objMapModel = qobject_cast<ObjectsMapModel *>(pm->parentItem()->model());
return new ValidatingPropertyContainerLineEdit(objMapModel->allSymbolicNames(), parent);
}
return QStyledItemDelegate::createEditor(parent, option, index);
}
default:
return QStyledItemDelegate::createEditor(parent, option, index);
}
}
void PropertyItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
if (index.column() == Operator) {
if (QComboBox *combo = qobject_cast<QComboBox *>(editor)) {
combo->setCurrentText(index.data().toString());
combo->showPopup();
}
} else if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor)) {
lineEdit->setText(index.data().toString());
} else {
QStyledItemDelegate::setEditorData(editor, index);
}
}
void PropertyItemDelegate::setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const
{
if (auto edit = qobject_cast<Utils::FancyLineEdit *>(editor)) {
if (!edit->isValid())
return;
}
QStyledItemDelegate::setModelData(editor, model, index);
}
/*********************************** ValidatingNameEdit ***************************************/
ValidatingPropertyNameLineEdit::ValidatingPropertyNameLineEdit(const QStringList &forbidden,
QWidget *parent)
: Utils::FancyLineEdit(parent)
, m_forbidden(forbidden)
{
setValidationFunction([this](FancyLineEdit *edit, QString * /*errorMessage*/) {
if (!edit)
return false;
const QRegularExpression identifier("^[a-zA2-Z0-9_]+$");
const QString &value = edit->text();
return !m_forbidden.contains(value) && identifier.match(value).hasMatch();
});
}
/*********************************** ValidatingContainerEdit **********************************/
ValidatingPropertyContainerLineEdit::ValidatingPropertyContainerLineEdit(const QStringList &allowed,
QWidget *parent)
: Utils::FancyLineEdit(parent)
, m_allowed(allowed)
{
setSpecialCompleter(new QCompleter(allowed, this));
setValidationFunction([this](FancyLineEdit *edit, QString * /*errorMessage*/) {
return edit && m_allowed.contains(edit->text());
});
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,74 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/fancylineedit.h>
#include <QStyledItemDelegate>
namespace Squish {
namespace Internal {
class PropertyItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
PropertyItemDelegate(QObject *parent = nullptr);
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const override;
};
class ValidatingPropertyNameLineEdit : public Utils::FancyLineEdit
{
Q_OBJECT
public:
ValidatingPropertyNameLineEdit(const QStringList &forbidden, QWidget *parent = nullptr);
private:
QStringList m_forbidden;
};
class ValidatingPropertyContainerLineEdit : public Utils::FancyLineEdit
{
Q_OBJECT
public:
ValidatingPropertyContainerLineEdit(const QStringList &allowed, QWidget *parent = nullptr);
private:
QStringList m_allowed;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,285 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "propertytreeitem.h"
#include <utils/qtcassert.h>
using namespace Utils;
namespace Squish {
namespace Internal {
// Squish IDE uses lower-case for "Is" - shall we too?
const QString Property::OPERATOR_IS = "Is";
const QString Property::OPERATOR_EQUALS = "Equals";
const QString Property::OPERATOR_REGEX = "RegEx";
const QString Property::OPERATOR_WILDCARD = "Wildcard";
/*************************************** Property *******************************************/
Property::Property() {}
Property::Property(const QByteArray &data)
{
const int equalsPosition = data.indexOf('=');
// no equals sign found or name is empty?
if (equalsPosition <= 0)
return;
QByteArray namePart = data.left(equalsPosition).trimmed();
QByteArray valuePart = data.mid(equalsPosition + 1).trimmed();
if (!valuePart.startsWith('\'') || !valuePart.endsWith('\''))
return;
const int namePartSize = namePart.size();
if (namePartSize > 1) {
char lastChar = namePart.at(namePartSize - 1);
if (lastChar == '~' || lastChar == '?') {
namePart.chop(1);
m_type = lastChar == '~' ? RegularExpression : Wildcard;
}
m_name = QLatin1String(namePart.trimmed());
}
m_value = QLatin1String(valuePart.mid(1, valuePart.size() - 2));
}
bool Property::set(const QString &propName, const QString &oper, const QString &propValue)
{
if (oper == "=")
m_type = Equals;
else if (oper == "~=")
m_type = RegularExpression;
else if (oper == "?=")
m_type = Wildcard;
else
return false;
m_name = propName;
m_value = propValue;
return true;
}
const QStringList Property::toStringList() const
{
QStringList result(m_name);
switch (m_type) {
case Equals:
if (isContainer() || isRelativeWidget())
result << OPERATOR_IS;
else
result << OPERATOR_EQUALS;
break;
case RegularExpression:
result << OPERATOR_REGEX;
break;
case Wildcard:
result << OPERATOR_WILDCARD;
break;
default:
QTC_ASSERT(false, result << QString());
break;
}
result << m_value;
return result;
}
bool Property::isContainer() const
{
static const char container[] = "container";
static const char window[] = "window";
return m_name == container || m_name == window;
}
bool Property::isRelativeWidget() const
{
static const QStringList relatives({"buddy", "aboveWidget", "leftWidget", "parentWidget"});
return relatives.contains(m_name);
}
PropertyType Property::typeFromString(const QString &typeString)
{
if (typeString == OPERATOR_EQUALS || typeString == OPERATOR_IS)
return Equals;
if (typeString == OPERATOR_REGEX)
return RegularExpression;
if (typeString == OPERATOR_WILDCARD)
return Wildcard;
QTC_ASSERT(false, return Equals);
}
const QString Property::toString() const
{
switch (m_type) {
case Equals:
return QString::fromLatin1("%1='%2'").arg(m_name, m_value);
case RegularExpression:
return QString::fromLatin1("%1~='%2'").arg(m_name, m_value);
case Wildcard:
return QString::fromLatin1("%1?='%2'").arg(m_name, m_value);
}
QTC_ASSERT(false, return QString());
}
/*********************************** PropertyTreeItem ***************************************/
enum ViewColumn { NameColumn, OperatorColumn, ValueColumn };
PropertyTreeItem::PropertyTreeItem(const Property &property, Qt::ItemFlags flags)
: m_property(property)
, m_flags(flags)
{}
QVariant PropertyTreeItem::data(int column, int role) const
{
if (role == Qt::DisplayRole && column >= NameColumn && column <= ValueColumn)
return m_property.toStringList().at(column);
return TreeItem::data(column, role);
}
bool PropertyTreeItem::setData(int column, const QVariant &data, int /*role*/)
{
// only accept untrimmed data for ValueColumn
const QString value = column == ValueColumn ? data.toString() : data.toString().trimmed();
if (value.isEmpty() && column != ValueColumn)
return false;
switch (column) {
case NameColumn:
m_property.m_name = value;
return true;
case OperatorColumn:
m_property.m_type = Property::typeFromString(value);
return true;
case ValueColumn:
m_property.m_value = value;
return true;
}
return false;
}
Qt::ItemFlags PropertyTreeItem::flags(int column) const
{
if (m_flags != Qt::NoItemFlags)
return m_flags;
return TreeItem::flags(column);
}
/*********************************** PropertiesModel ****************************************/
PropertiesModel::PropertiesModel(ObjectsMapTreeItem *parentItem)
: TreeModel<PropertyTreeItem>(new PropertyTreeItem({}))
, m_parentItem(parentItem)
{
setHeader(QStringList() << tr("Name") << tr("Operator") << tr("Value"));
}
bool PropertiesModel::setData(const QModelIndex &idx, const QVariant &data, int role)
{
// only editing is supported
if (role != Qt::EditRole || !data.isValid())
return false;
const int column = idx.column();
if (column < NameColumn || column > ValueColumn)
return false;
const QString old = idx.data().toString();
bool result = TreeModel::setData(idx, data, role);
if (result)
emit propertyChanged(m_parentItem, old, data.toString(), idx.row(), idx.column());
return result;
}
void PropertiesModel::addNewProperty(PropertyTreeItem *item)
{
QTC_ASSERT(item, return );
QTC_ASSERT(rootItem(), return );
rootItem()->appendChild(item);
emit propertyAdded(m_parentItem);
}
void PropertiesModel::removeProperty(PropertyTreeItem *item)
{
QTC_ASSERT(item, return );
Property property = item->property();
delete takeItem(item);
emit propertyRemoved(m_parentItem, property);
}
void PropertiesModel::modifySpecialProperty(const QString &oldValue, const QString &newValue)
{
TreeItem *root = rootItem();
QTC_ASSERT(root, return );
TreeItem *itemToChange = root->findChildAtLevel(1, [oldValue](TreeItem *child) {
auto propertyItem = static_cast<PropertyTreeItem *>(child);
Property property = propertyItem->property();
return (property.m_value == oldValue
&& (property.isContainer() || property.isRelativeWidget()));
});
if (!itemToChange)
return;
auto propertyItem = static_cast<PropertyTreeItem *>(itemToChange);
propertyItem->setData(ValueColumn, newValue, Qt::EditRole);
const QModelIndex idx = indexForItem(propertyItem);
emit propertyChanged(m_parentItem, oldValue, newValue, idx.row(), idx.column());
}
QStringList PropertiesModel::allPropertyNames() const
{
TreeItem *root = rootItem();
if (!root)
return QStringList();
QStringList result;
result.reserve(root->childCount());
root->forChildrenAtLevel(1, [&result](TreeItem *child) {
result.append(child->data(NameColumn, Qt::DisplayRole).toString());
});
return result;
}
/********************************* PropertiesSortModel **************************************/
PropertiesSortModel::PropertiesSortModel(QObject *parent)
: QSortFilterProxyModel(parent)
{}
bool PropertiesSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
return left.data().toString() > right.data().toString();
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,117 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/treemodel.h>
#include <QSortFilterProxyModel>
namespace Squish {
namespace Internal {
class ObjectsMapTreeItem;
enum PropertyType { Equals, RegularExpression, Wildcard };
class Property
{
public:
Property();
Property(const QByteArray &data);
bool set(const QString &propName, const QString &oper, const QString &propValue);
const QStringList toStringList() const;
bool isContainer() const;
bool isRelativeWidget() const;
static PropertyType typeFromString(const QString &typeString);
const QString toString() const;
QString m_name;
PropertyType m_type = Equals;
QString m_value;
static const QString OPERATOR_IS;
static const QString OPERATOR_EQUALS;
static const QString OPERATOR_REGEX;
static const QString OPERATOR_WILDCARD;
};
typedef QList<Property> PropertyList;
class PropertyTreeItem : public Utils::TreeItem
{
public:
explicit PropertyTreeItem(const Property &property,
Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsEditable
| Qt::ItemIsSelectable);
QVariant data(int column, int role) const override;
bool setData(int column, const QVariant &data, int role) override;
Qt::ItemFlags flags(int column) const override;
Property property() const { return m_property; }
private:
Property m_property;
Qt::ItemFlags m_flags = Qt::NoItemFlags;
};
class PropertiesModel : public Utils::TreeModel<PropertyTreeItem>
{
Q_OBJECT
public:
PropertiesModel(ObjectsMapTreeItem *parentItem);
bool setData(const QModelIndex &idx, const QVariant &data, int role) override;
ObjectsMapTreeItem *parentItem() const { return m_parentItem; }
void addNewProperty(PropertyTreeItem *item);
void removeProperty(PropertyTreeItem *item);
void modifySpecialProperty(const QString &oldValue, const QString &newValue);
QStringList allPropertyNames() const;
signals:
void propertyChanged(
ObjectsMapTreeItem *item, const QString &old, const QString &modified, int row, int column);
void propertyRemoved(ObjectsMapTreeItem *item, const Property &property);
void propertyAdded(ObjectsMapTreeItem *item);
private:
ObjectsMapTreeItem *m_parentItem; // not owned
};
class PropertiesSortModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
PropertiesSortModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,63 @@
import qbs
QtcPlugin {
name: "Squish"
Depends { name: "Core" }
Depends { name: "Utils" }
Depends { name: "Qt.widgets" }
files: [
"squish.qrc",
"squishplugin_global.h",
"squishconstants.h",
"squishplugin.cpp",
"squishplugin.h",
"squishsettings.cpp",
"squishsettings.h",
"squishsettingspage.cpp",
"squishsettingspage.h",
"squishsettingspage.ui",
"squishnavigationwidget.cpp",
"squishnavigationwidget.h",
"squishoutputpane.cpp",
"squishoutputpane.h",
"squishtesttreemodel.cpp",
"squishtesttreemodel.h",
"squishtesttreeview.cpp",
"squishtesttreeview.h",
"squishfilehandler.cpp",
"squishfilehandler.h",
"opensquishsuitesdialog.cpp",
"opensquishsuitesdialog.h",
"opensquishsuitesdialog.ui",
"squishutils.cpp",
"squishutils.h",
"squishtools.cpp",
"squishtools.h",
"squishxmloutputhandler.cpp",
"squishxmloutputhandler.h",
"testresult.cpp",
"testresult.h",
"squishresultmodel.cpp",
"squishresultmodel.h",
"deletesymbolicnamedialog.cpp",
"deletesymbolicnamedialog.h",
"deletesymbolicnamedialog.ui",
"objectsmapdocument.cpp",
"objectsmapdocument.h",
"objectsmaptreeitem.cpp",
"objectsmaptreeitem.h",
"propertytreeitem.cpp",
"propertytreeitem.h",
"objectsmapeditorwidget.cpp",
"objectsmapeditorwidget.h",
"objectsmapeditor.cpp",
"objectsmapeditor.h",
"propertyitemdelegate.cpp",
"propertyitemdelegate.h",
"symbolnameitemdelegate.cpp",
"symbolnameitemdelegate.h"
]
}

View File

@@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/squish/">
<file>images/settingscategory_squish.png</file>
<file>images/settingscategory_squish@2x.png</file>
<file>images/objectsmap.png</file>
<file>images/play.png</file>
<file>images/record.png</file>
<file>images/jumpTo.png</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,44 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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.
**
****************************************************************************/
#ifndef SQUISHCONSTANTS_H
#define SQUISHCONSTANTS_H
#include <QtGlobal>
namespace Squish {
namespace Constants {
const char SQUISH_ID[] = "SquishPlugin.Squish";
const char SQUISH_CONTEXT[] = "Squish";
const char SQUISH_SETTINGS_CATEGORY[] = "ZYY.Squish";
// MIME type defined by Squish plugin
const char SQUISH_OBJECTSMAP_MIMETYPE[] = "text/squish-objectsmap";
const char OBJECTSMAP_EDITOR_ID[] = "Squish.ObjectsMapEditor";
} // namespace Constants
} // namespace Squish
#endif // SQUISHCONSTANTS_H

View File

@@ -0,0 +1,269 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishfilehandler.h"
#include "opensquishsuitesdialog.h"
#include "squishconstants.h"
#include "squishtesttreemodel.h"
#include "squishtools.h"
#include "squishutils.h"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <utils/qtcassert.h>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
namespace Squish {
namespace Internal {
static SquishFileHandler *m_instance = nullptr;
SquishFileHandler::SquishFileHandler(QObject *parent)
: QObject(parent)
, m_squishTools(new SquishTools)
{
m_instance = this;
}
SquishFileHandler::~SquishFileHandler()
{
delete m_squishTools;
}
SquishFileHandler *SquishFileHandler::instance()
{
if (!m_instance)
m_instance = new SquishFileHandler;
return m_instance;
}
SquishTestTreeItem *createTestTreeItem(const QString &name,
const QString &filePath,
const QStringList &cases)
{
SquishTestTreeItem *item = new SquishTestTreeItem(name, SquishTestTreeItem::SquishSuite);
item->setFilePath(filePath);
for (const QString &testCase : cases) {
SquishTestTreeItem *child = new SquishTestTreeItem(QFileInfo(testCase).dir().dirName(),
SquishTestTreeItem::SquishTestCase);
child->setFilePath(testCase);
item->appendChild(child);
}
return item;
}
void SquishFileHandler::modifySuiteItem(const QString &suiteName,
const QString &filePath,
const QStringList &cases)
{
SquishTestTreeItem *item = createTestTreeItem(suiteName, filePath, cases);
// TODO update file watcher
m_suites.insert(suiteName, filePath);
emit suiteTreeItemModified(item, suiteName);
}
void SquishFileHandler::openTestSuites()
{
OpenSquishSuitesDialog dialog;
dialog.exec();
QMessageBox::StandardButton replaceSuite = QMessageBox::NoButton;
const QStringList chosenSuites = dialog.chosenSuites();
for (const QString &suite : chosenSuites) {
const QDir suiteDir(suite);
const QString suiteName = suiteDir.dirName();
const QStringList cases = SquishUtils::validTestCases(suite);
const QFileInfo suiteConf(suiteDir, "suite.conf");
if (m_suites.contains(suiteName)) {
if (replaceSuite == QMessageBox::YesToAll) {
modifySuiteItem(suiteName, suiteConf.absoluteFilePath(), cases);
} else if (replaceSuite != QMessageBox::NoToAll) {
replaceSuite
= QMessageBox::question(Core::ICore::dialogParent(),
tr("Suite Already Open"),
tr("A test suite with the name \"%1\" is already open."
"\nClose the opened test suite and replac it "
"with the new one?")
.arg(suiteName),
QMessageBox::Yes | QMessageBox::YesToAll
| QMessageBox::No | QMessageBox::NoToAll,
QMessageBox::No);
if (replaceSuite == QMessageBox::YesToAll || replaceSuite == QMessageBox::Yes)
modifySuiteItem(suiteName, suiteConf.absoluteFilePath(), cases);
}
} else {
SquishTestTreeItem *item = createTestTreeItem(suiteName,
suiteConf.absoluteFilePath(),
cases);
// TODO add file watcher
m_suites.insert(suiteName, suiteConf.absoluteFilePath());
emit testTreeItemCreated(item);
}
}
emit suitesOpened();
}
void SquishFileHandler::closeTestSuite(const QString &suiteName)
{
if (!m_suites.contains(suiteName))
return;
// TODO close respective editors if there are any
// TODO remove file watcher
m_suites.remove(suiteName);
emit suiteTreeItemRemoved(suiteName);
}
void SquishFileHandler::closeAllTestSuites()
{
// TODO close respective editors if there are any
// TODO remove file watcher
const QStringList &suiteNames = m_suites.keys();
m_suites.clear();
for (const QString &suiteName : suiteNames)
emit suiteTreeItemRemoved(suiteName);
}
void SquishFileHandler::runTestCase(const QString &suiteName, const QString &testCaseName)
{
QTC_ASSERT(!suiteName.isEmpty() && !testCaseName.isEmpty(), return );
if (m_squishTools->state() != SquishTools::Idle)
return;
const QDir suitePath = QFileInfo(m_suites.value(suiteName)).absoluteDir();
if (!suitePath.exists() || !suitePath.isReadable()) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Test Suite Path Not Accessible"),
tr("The path \"%1\" does not exist or is not accessible.\n"
"Refusing to run test case \"%2\".")
.arg(QDir::toNativeSeparators(suitePath.absolutePath()))
.arg(testCaseName));
return;
}
m_squishTools->runTestCases(suitePath.absolutePath(), QStringList(testCaseName));
}
void SquishFileHandler::runTestSuite(const QString &suiteName)
{
QTC_ASSERT(!suiteName.isEmpty(), return );
if (m_squishTools->state() != SquishTools::Idle)
return;
const QString suiteConf = m_suites.value(suiteName);
const QDir suitePath = QFileInfo(suiteConf).absoluteDir();
if (!suitePath.exists() || !suitePath.isReadable()) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Test Suite Path Not Accessible"),
tr("The path \"%1\" does not exist or is not accessible.\n"
"Refusing to run test cases.")
.arg(QDir::toNativeSeparators(suitePath.absolutePath())));
return;
}
QStringList testCases = SquishTestTreeModel::instance()->getSelectedSquishTestCases(suiteConf);
m_squishTools->runTestCases(suitePath.absolutePath(), testCases);
}
void addAllEntriesRecursively(SquishTestTreeItem *item)
{
QDir folder(item->filePath());
const QFileInfoList entries = folder.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
for (const QFileInfo &info : entries) {
const QString &path = info.absoluteFilePath();
// TODO improve this later? Squish refuses directories containing Squish test suites
const bool isDir = info.isDir();
if (!info.isFile() && !isDir)
continue;
SquishTestTreeItem *child
= new SquishTestTreeItem(info.fileName(),
isDir ? SquishTestTreeItem::SquishSharedFolder
: SquishTestTreeItem::SquishSharedFile);
child->setFilePath(path);
if (info.isDir())
addAllEntriesRecursively(child);
item->appendChild(child);
}
}
void SquishFileHandler::addSharedFolder()
{
const QString &chosen = QFileDialog::getExistingDirectory(Core::ICore::dialogParent(),
tr("Select Global Script Folder"));
if (chosen.isEmpty())
return;
if (m_sharedFolders.contains(chosen))
return;
m_sharedFolders.append(chosen);
SquishTestTreeItem *item = new SquishTestTreeItem(chosen,
SquishTestTreeItem::SquishSharedFolder);
item->setFilePath(chosen);
addAllEntriesRecursively(item);
emit testTreeItemCreated(item);
}
bool SquishFileHandler::removeSharedFolder(const QString &folder)
{
if (m_sharedFolders.contains(folder))
return m_sharedFolders.removeOne(folder);
return false;
}
void SquishFileHandler::removeAllSharedFolders()
{
m_sharedFolders.clear();
}
void SquishFileHandler::openObjectsMap(const QString &suiteName)
{
QTC_ASSERT(!suiteName.isEmpty(), return );
const Utils::FilePath objectsMapPath = Utils::FilePath::fromString(
SquishUtils::objectsMapPath(m_suites.value(suiteName)));
if (!objectsMapPath.isEmpty() && objectsMapPath.exists()) {
if (!Core::EditorManager::openEditor(objectsMapPath, Constants::OBJECTSMAP_EDITOR_ID)) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Error"),
tr("Failed to open objects.map file at \"%1\".")
.arg(objectsMapPath.toUserOutput()));
}
}
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,73 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishtesttreemodel.h"
#include <QMap>
#include <QObject>
#include <QString>
namespace Squish {
namespace Internal {
class SquishTools;
class SquishFileHandler : public QObject
{
Q_OBJECT
public:
explicit SquishFileHandler(QObject *parent = nullptr);
~SquishFileHandler() override;
static SquishFileHandler *instance();
void openTestSuites();
void closeTestSuite(const QString &suiteName);
void closeAllTestSuites();
void runTestCase(const QString &suiteName, const QString &testCaseName);
void runTestSuite(const QString &suiteName);
void addSharedFolder();
bool removeSharedFolder(const QString &folder);
void removeAllSharedFolders();
void openObjectsMap(const QString &suiteName);
signals:
void testTreeItemCreated(SquishTestTreeItem *item);
void suiteTreeItemRemoved(const QString &filePath);
void suiteTreeItemModified(SquishTestTreeItem *item, const QString &displayName);
void suitesOpened();
private:
QMap<QString, QString> m_suites;
QStringList m_sharedFolders;
SquishTools *m_squishTools;
void modifySuiteItem(const QString &suiteName,
const QString &filePath,
const QStringList &cases);
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,320 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishnavigationwidget.h"
#include "squishconstants.h"
#include "squishfilehandler.h"
#include "squishtesttreemodel.h"
#include "squishtesttreeview.h"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/find/itemviewfind.h>
#include <coreplugin/icore.h>
#include <utils/qtcassert.h>
#include <QDir>
#include <QHeaderView>
#include <QMenu>
#include <QMessageBox>
#include <QVBoxLayout>
namespace Squish {
namespace Internal {
const int defaultSectionSize = 17;
SquishNavigationWidget::SquishNavigationWidget(QWidget *parent)
: QWidget(parent)
{
setWindowTitle(tr("Squish"));
m_view = new SquishTestTreeView(this);
m_model = SquishTestTreeModel::instance();
m_sortModel = new SquishTestTreeSortModel(m_model, m_model);
m_sortModel->setDynamicSortFilter(true);
m_view->setModel(m_sortModel);
m_view->setSortingEnabled(true);
m_view->setItemDelegate(new SquishTestTreeItemDelegate(this));
QHeaderView *header = new QHeaderView(Qt::Horizontal, m_view);
header->setModel(m_model);
header->setDefaultSectionSize(0);
header->setSectionResizeMode(0, QHeaderView::Stretch);
header->setSectionResizeMode(1, QHeaderView::Fixed);
header->setSectionResizeMode(2, QHeaderView::Fixed);
m_view->setHeader(header);
m_view->setHeaderHidden(true);
QVBoxLayout *layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(Core::ItemViewFind::createSearchableWrapper(m_view));
setLayout(layout);
connect(m_view, &QTreeView::expanded, this, &SquishNavigationWidget::onExpanded);
connect(m_view, &QTreeView::collapsed, this, &SquishNavigationWidget::onCollapsed);
connect(m_view, &QTreeView::activated, this, &SquishNavigationWidget::onItemActivated);
connect(m_model,
&QAbstractItemModel::rowsInserted,
this,
&SquishNavigationWidget::onRowsInserted);
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &SquishNavigationWidget::onRowsRemoved);
connect(m_view,
&SquishTestTreeView::runTestCase,
SquishFileHandler::instance(),
&SquishFileHandler::runTestCase);
connect(m_view,
&SquishTestTreeView::runTestSuite,
SquishFileHandler::instance(),
&SquishFileHandler::runTestSuite);
connect(m_view,
&SquishTestTreeView::openObjectsMap,
SquishFileHandler::instance(),
&SquishFileHandler::openObjectsMap);
connect(SquishFileHandler::instance(), &SquishFileHandler::suitesOpened, this, [this]() {
const QModelIndex &suitesIndex = m_view->model()->index(1, 0);
if (m_view->isExpanded(suitesIndex))
onExpanded(suitesIndex);
});
}
SquishNavigationWidget::~SquishNavigationWidget() {}
void SquishNavigationWidget::contextMenuEvent(QContextMenuEvent *event)
{
QMenu menu;
// item specific menu entries
const QModelIndexList list = m_view->selectionModel()->selectedIndexes();
if (list.size() == SquishTestTreeModel::COLUMN_COUNT) {
QRect rect(m_view->visualRect(list.first()));
if (rect.contains(event->pos())) {
const QModelIndex &idx = list.first();
const int type = idx.data(TypeRole).toInt();
switch (type) {
case SquishTestTreeItem::SquishTestCase: {
const QString caseName = idx.data(DisplayNameRole).toString();
const QString suiteName = idx.parent().data(DisplayNameRole).toString();
QAction *runThisTestCase = new QAction(tr("Run This Test Case"), &menu);
menu.addAction(runThisTestCase);
QAction *deleteTestCase = new QAction(tr("Delete Test Case"), &menu);
menu.addAction(deleteTestCase);
menu.addSeparator();
connect(runThisTestCase, &QAction::triggered, [suiteName, caseName]() {
SquishFileHandler::instance()->runTestCase(suiteName, caseName);
});
break;
}
case SquishTestTreeItem::SquishSuite: {
const QString suiteName = idx.data(DisplayNameRole).toString();
QAction *runThisTestSuite = new QAction(tr("Run This Test Suite"), &menu);
menu.addAction(runThisTestSuite);
menu.addSeparator();
QAction *addNewTestCase = new QAction(tr("Add New Test Case..."), &menu);
menu.addAction(addNewTestCase);
QAction *closeTestSuite = new QAction(tr("Close Test Suite"), &menu);
menu.addAction(closeTestSuite);
QAction *deleteTestSuite = new QAction(tr("Delete Test Suite"), &menu);
menu.addAction(deleteTestSuite);
menu.addSeparator();
connect(runThisTestSuite, &QAction::triggered, [suiteName]() {
SquishFileHandler::instance()->runTestSuite(suiteName);
});
connect(closeTestSuite, &QAction::triggered, [suiteName]() {
SquishFileHandler::instance()->closeTestSuite(suiteName);
});
break;
}
case SquishTestTreeItem::SquishSharedFile: {
QAction *deleteSharedFile = new QAction(tr("Delete Shared File"), &menu);
menu.addAction(deleteSharedFile);
break;
}
case SquishTestTreeItem::SquishSharedFolder: {
QAction *addSharedFile = new QAction(tr("Add Shared File"), &menu);
menu.addAction(addSharedFile);
// only add the action 'Remove Shared Folder' for top-level shared folders, not
// to their recursively added sub-folders
if (idx.parent().data(TypeRole).toInt() == SquishTestTreeItem::Root) {
QAction *removeSharedFolder = new QAction(tr("Remove Shared Folder"), &menu);
menu.addAction(removeSharedFolder);
menu.addSeparator();
connect(removeSharedFolder, &QAction::triggered, this, [this, idx]() {
onRemoveSharedFolderTriggered(idx.row(), idx.parent());
});
}
break;
}
default:
break;
}
}
}
const QModelIndex &foldersIndex = m_view->model()->index(0, 0);
const QModelIndex &suitesIndex = m_view->model()->index(1, 0);
// general squish related menu entries
QAction *openSquishSuites = new QAction(tr("Open Squish Suites..."), &menu);
menu.addAction(openSquishSuites);
QAction *createNewTestSuite = new QAction(tr("Create New Test Suite..."), &menu);
menu.addAction(createNewTestSuite);
connect(openSquishSuites,
&QAction::triggered,
SquishFileHandler::instance(),
&SquishFileHandler::openTestSuites);
if (m_view->model()->rowCount(suitesIndex) > 0) {
menu.addSeparator();
QAction *closeAllSuites = new QAction(tr("Close All Test Suites"), &menu);
menu.addAction(closeAllSuites);
connect(closeAllSuites, &QAction::triggered, this, [this]() {
if (QMessageBox::question(this,
tr("Close All Test Suites"),
tr("Close all test suites?"
/*"\nThis will close all related files as well."*/))
== QMessageBox::Yes)
SquishFileHandler::instance()->closeAllTestSuites();
});
}
menu.addSeparator();
QAction *addSharedFolder = new QAction(tr("Add Shared Folder..."), &menu);
menu.addAction(addSharedFolder);
connect(addSharedFolder,
&QAction::triggered,
SquishFileHandler::instance(),
&SquishFileHandler::addSharedFolder);
if (m_view->model()->rowCount(foldersIndex) > 0) {
menu.addSeparator();
QAction *removeAllFolders = new QAction(tr("Remove All Shared Folders"), &menu);
menu.addAction(removeAllFolders);
connect(removeAllFolders,
&QAction::triggered,
this,
&SquishNavigationWidget::onRemoveAllSharedFolderTriggered);
}
menu.exec(mapToGlobal(event->pos()));
}
QList<QToolButton *> SquishNavigationWidget::createToolButtons()
{
QList<QToolButton *> toolButtons;
return toolButtons;
}
void SquishNavigationWidget::onItemActivated(const QModelIndex &idx)
{
if (!idx.isValid())
return;
SquishTestTreeItem *item = static_cast<SquishTestTreeItem *>(m_sortModel->itemFromIndex(idx));
if (item->type() == SquishTestTreeItem::SquishSharedFolder)
return;
if (!item->filePath().isEmpty())
Core::EditorManager::openEditor(Utils::FilePath::fromString(item->filePath()));
}
void SquishNavigationWidget::onExpanded(const QModelIndex &idx)
{
if (idx.data().toString().startsWith(tr("Test Suites")))
m_view->header()->setDefaultSectionSize(defaultSectionSize);
}
void SquishNavigationWidget::onCollapsed(const QModelIndex &idx)
{
if (idx.data().toString().startsWith(tr("Test Suites")))
m_view->header()->setDefaultSectionSize(0);
}
void SquishNavigationWidget::onRowsInserted(const QModelIndex &parent, int, int)
{
if (parent.isValid() && parent.data().toString().startsWith(tr("Test Suites")))
if (m_view->isExpanded(parent) && m_model->rowCount(parent))
m_view->header()->setDefaultSectionSize(defaultSectionSize);
}
void SquishNavigationWidget::onRowsRemoved(const QModelIndex &parent, int, int)
{
if (parent.isValid() && parent.data().toString().startsWith(tr("Test Suites")))
if (m_model->rowCount(parent) == 0)
m_view->header()->setDefaultSectionSize(0);
}
void SquishNavigationWidget::onRemoveSharedFolderTriggered(int row, const QModelIndex &parent)
{
const QString folder = m_model->index(row, 0, parent).data().toString();
QTC_ASSERT(!folder.isEmpty(), return );
if (QMessageBox::question(Core::ICore::dialogParent(),
tr("Remove Shared Folder"),
tr("Remove \"%1\" from the list of shared folders?")
.arg(QDir::toNativeSeparators(folder)))
!= QMessageBox::Yes) {
return;
}
const QModelIndex &realIdx = m_sortModel->mapToSource(m_model->index(row, 0, parent));
if (SquishFileHandler::instance()->removeSharedFolder(folder))
m_model->removeTreeItem(realIdx.row(), realIdx.parent());
}
void SquishNavigationWidget::onRemoveAllSharedFolderTriggered()
{
if (QMessageBox::question(Core::ICore::dialogParent(),
tr("Remove All Shared Folders"),
tr("Remove all shared folders?"))
!= QMessageBox::Yes) {
return;
}
SquishFileHandler::instance()->removeAllSharedFolders();
m_model->removeAllSharedFolders();
}
SquishNavigationWidgetFactory::SquishNavigationWidgetFactory()
{
setDisplayName(tr("Squish"));
setId(Squish::Constants::SQUISH_ID);
setPriority(777);
}
Core::NavigationView SquishNavigationWidgetFactory::createWidget()
{
SquishNavigationWidget *squishNavigationWidget = new SquishNavigationWidget;
Core::NavigationView view;
view.widget = squishNavigationWidget;
view.dockToolBarWidgets = squishNavigationWidget->createToolButtons();
return view;
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,76 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <coreplugin/inavigationwidgetfactory.h>
#include <utils/navigationtreeview.h>
QT_BEGIN_NAMESPACE
class QToolButton;
QT_END_NAMESPACE
namespace Squish {
namespace Internal {
class SquishTestTreeModel;
class SquishTestTreeSortModel;
class SquishTestTreeView;
class SquishNavigationWidget : public QWidget
{
Q_OBJECT
public:
explicit SquishNavigationWidget(QWidget *parent = nullptr);
~SquishNavigationWidget() override;
void contextMenuEvent(QContextMenuEvent *event) override;
static QList<QToolButton *> createToolButtons();
private:
void onItemActivated(const QModelIndex &idx);
void onExpanded(const QModelIndex &idx);
void onCollapsed(const QModelIndex &idx);
void onRowsInserted(const QModelIndex &parent, int, int);
void onRowsRemoved(const QModelIndex &parent, int, int);
void onRemoveSharedFolderTriggered(int row, const QModelIndex &parent);
void onRemoveAllSharedFolderTriggered();
SquishTestTreeView *m_view;
SquishTestTreeModel *m_model; // not owned
SquishTestTreeSortModel *m_sortModel;
};
class SquishNavigationWidgetFactory : public Core::INavigationWidgetFactory
{
Q_OBJECT
public:
SquishNavigationWidgetFactory();
private:
Core::NavigationView createWidget() override;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,414 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishoutputpane.h"
#include "squishresultmodel.h"
#include "testresult.h"
#include <QHeaderView>
#include <QLabel>
#include <QMenu>
#include <QPlainTextEdit>
#include <QTabWidget>
#include <QToolButton>
#include <QVBoxLayout>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icontext.h>
#include <utils/itemviews.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
namespace Squish {
namespace Internal {
static SquishOutputPane *m_instance = nullptr;
SquishOutputPane::SquishOutputPane(QObject *parent)
: Core::IOutputPane(parent)
, m_context(new Core::IContext(this))
{
m_outputPane = new QTabWidget;
m_outputPane->setDocumentMode(true);
m_outputWidget = new QWidget;
QVBoxLayout *outputLayout = new QVBoxLayout;
outputLayout->setContentsMargins(0, 0, 0, 0);
outputLayout->setSpacing(0);
m_outputWidget->setLayout(outputLayout);
QPalette pal;
pal.setColor(QPalette::Window, Utils::creatorTheme()->color(Utils::Theme::InfoBarBackground));
pal.setColor(QPalette::WindowText, Utils::creatorTheme()->color(Utils::Theme::InfoBarText));
m_summaryWidget = new QFrame;
m_summaryWidget->setPalette(pal);
m_summaryWidget->setAutoFillBackground(true);
QHBoxLayout *summaryLayout = new QHBoxLayout;
summaryLayout->setContentsMargins(6, 6, 6, 6);
m_summaryWidget->setLayout(summaryLayout);
m_summaryLabel = new QLabel;
m_summaryLabel->setPalette(pal);
summaryLayout->addWidget(m_summaryLabel);
m_summaryWidget->setVisible(false);
outputLayout->addWidget(m_summaryWidget);
m_treeView = new Utils::TreeView(m_outputWidget);
m_treeView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_treeView->setAlternatingRowColors(true);
m_model = new SquishResultModel(this);
m_filterModel = new SquishResultFilterModel(m_model, this);
m_filterModel->setDynamicSortFilter(true);
m_treeView->setModel(m_filterModel);
QHeaderView *header = m_treeView->header();
header->setSectionsMovable(false);
header->setStretchLastSection(false);
header->setSectionResizeMode(QHeaderView::ResizeToContents);
header->setSectionResizeMode(1, QHeaderView::Interactive);
m_treeView->setHeaderHidden(true);
outputLayout->addWidget(m_treeView);
createToolButtons();
m_runnerServerLog = new QPlainTextEdit;
m_runnerServerLog->setMaximumBlockCount(10000);
m_runnerServerLog->setReadOnly(true);
m_outputPane->addTab(m_outputWidget, tr("Test Results"));
m_outputPane->addTab(m_runnerServerLog, tr("Runner/Server Log"));
connect(m_outputPane, &QTabWidget::currentChanged, this, [this]() { navigateStateChanged(); });
connect(m_treeView, &Utils::TreeView::activated, this, &SquishOutputPane::onItemActivated);
connect(header, &QHeaderView::sectionResized, this, &SquishOutputPane::onSectionResized);
connect(m_model, &SquishResultModel::requestExpansion, this, [this](QModelIndex idx) {
m_treeView->expand(m_filterModel->mapFromSource(idx));
});
connect(m_model,
&SquishResultModel::resultTypeCountUpdated,
this,
&SquishOutputPane::updateSummaryLabel);
}
SquishOutputPane *SquishOutputPane::instance()
{
if (!m_instance)
m_instance = new SquishOutputPane;
return m_instance;
}
QWidget *SquishOutputPane::outputWidget(QWidget *parent)
{
if (m_outputPane)
m_outputPane->setParent(parent);
else
qWarning("This should not happen");
return m_outputPane;
}
QList<QWidget *> SquishOutputPane::toolBarWidgets() const
{
return QList<QWidget *>() << m_filterButton << m_expandAll << m_collapseAll;
}
QString SquishOutputPane::displayName() const
{
return tr("Squish");
}
int SquishOutputPane::priorityInStatusBar() const
{
return -777;
}
void SquishOutputPane::clearContents()
{
if (m_outputPane->currentIndex() == 0)
clearOldResults();
else if (m_outputPane->currentIndex() == 1)
m_runnerServerLog->clear();
}
void SquishOutputPane::visibilityChanged(bool visible)
{
Q_UNUSED(visible)
}
void SquishOutputPane::setFocus()
{
if (m_outputPane->currentIndex() == 0)
m_treeView->setFocus();
else if (m_outputPane->currentIndex() == 1)
m_runnerServerLog->setFocus();
}
bool SquishOutputPane::hasFocus() const
{
return m_treeView->hasFocus() || m_runnerServerLog->hasFocus();
}
bool SquishOutputPane::canFocus() const
{
return true;
}
bool SquishOutputPane::canNavigate() const
{
return m_outputPane->currentIndex() == 0; // only support navigation for test results
}
bool SquishOutputPane::canNext() const
{
return m_filterModel->hasResults();
}
bool SquishOutputPane::canPrevious() const
{
return m_filterModel->hasResults();
}
void SquishOutputPane::goToNext()
{
if (!canNext())
return;
const QModelIndex currentIndex = m_treeView->currentIndex();
QModelIndex nextCurrentIndex;
if (currentIndex.isValid()) {
// try to set next to first child or next sibling
if (m_filterModel->rowCount(currentIndex)) {
nextCurrentIndex = m_filterModel->index(0, 0, currentIndex);
} else {
nextCurrentIndex = currentIndex.sibling(currentIndex.row() + 1, 0);
// if it had no sibling check siblings of parent (and grandparents if necessary)
if (!nextCurrentIndex.isValid()) {
QModelIndex parent = currentIndex.parent();
do {
if (!parent.isValid())
break;
nextCurrentIndex = parent.sibling(parent.row() + 1, 0);
parent = parent.parent();
} while (!nextCurrentIndex.isValid());
}
}
}
// if we have no current or could not find a next one, use the first item of the whole tree
if (!nextCurrentIndex.isValid()) {
Utils::TreeItem *rootItem = m_model->itemForIndex(QModelIndex());
// if the tree does not contain any item - don't do anything
if (!rootItem || !rootItem->childCount())
return;
nextCurrentIndex = m_filterModel->mapFromSource(m_model->indexForItem(rootItem->childAt(0)));
}
m_treeView->setCurrentIndex(nextCurrentIndex);
onItemActivated(nextCurrentIndex);
}
void SquishOutputPane::goToPrev()
{
if (!canPrevious())
return;
const QModelIndex currentIndex = m_treeView->currentIndex();
QModelIndex nextCurrentIndex;
if (currentIndex.isValid()) {
// try to set next to prior sibling or parent
if (currentIndex.row() > 0) {
nextCurrentIndex = currentIndex.sibling(currentIndex.row() - 1, 0);
// if the sibling has children, use the last one
while (int rowCount = m_filterModel->rowCount(nextCurrentIndex))
nextCurrentIndex = m_filterModel->index(rowCount - 1, 0, nextCurrentIndex);
} else {
nextCurrentIndex = currentIndex.parent();
}
}
// if we have no current or didn't find a sibling/parent use the last item of the whole tree
if (!nextCurrentIndex.isValid()) {
const QModelIndex rootIdx = m_filterModel->index(0, 0);
// if the tree does not contain any item - don't do anything
if (!rootIdx.isValid())
return;
// get the last (visible) top level index
nextCurrentIndex = m_filterModel->index(m_filterModel->rowCount(QModelIndex()) - 1, 0);
// step through until end
while (int rowCount = m_filterModel->rowCount(nextCurrentIndex))
nextCurrentIndex = m_filterModel->index(rowCount - 1, 0, nextCurrentIndex);
}
m_treeView->setCurrentIndex(nextCurrentIndex);
onItemActivated(nextCurrentIndex);
}
void SquishOutputPane::addResultItem(SquishResultItem *item)
{
m_model->addResultItem(item);
m_treeView->setHeaderHidden(false);
if (!m_treeView->isVisible())
popup(Core::IOutputPane::NoModeSwitch);
flash();
navigateStateChanged();
}
void SquishOutputPane::addLogOutput(const QString &output)
{
m_runnerServerLog->appendPlainText(output);
}
void SquishOutputPane::onTestRunFinished()
{
m_model->expandVisibleRootItems();
m_summaryWidget->setVisible(true);
updateSummaryLabel();
}
void SquishOutputPane::updateSummaryLabel()
{
if (m_summaryWidget->isVisible()) {
const int passes = m_model->resultTypeCount(Result::Pass)
+ m_model->resultTypeCount(Result::ExpectedFail);
const int fails = m_model->resultTypeCount(Result::Fail)
+ m_model->resultTypeCount(Result::UnexpectedPass);
const QString labelText = tr("<p><b>Test summary:</b>&nbsp;&nbsp; %1 passes, %2 fails, "
"%3 fatals, %4 errors, %5 warnings.</p>")
.arg(passes)
.arg(fails)
.arg(m_model->resultTypeCount(Result::Fatal))
.arg(m_model->resultTypeCount(Result::Error))
.arg(m_model->resultTypeCount(Result::Warn));
m_summaryLabel->setText(labelText);
}
}
void SquishOutputPane::clearOldResults()
{
m_treeView->setHeaderHidden(true);
m_summaryWidget->setVisible(false);
m_filterModel->clearResults();
navigateStateChanged();
}
void SquishOutputPane::createToolButtons()
{
m_expandAll = new QToolButton(m_treeView);
m_expandAll->setIcon(Utils::Icons::EXPAND_TOOLBAR.icon());
m_expandAll->setToolTip(tr("Expand All"));
m_collapseAll = new QToolButton(m_treeView);
m_collapseAll->setIcon(Utils::Icons::COLLAPSE_TOOLBAR.icon());
m_collapseAll->setToolTip(tr("Collapse All"));
m_filterButton = new QToolButton(m_treeView);
m_filterButton->setIcon(Utils::Icons::FILTER.icon());
m_filterButton->setToolTip(tr("Filter Test Results"));
m_filterButton->setProperty("noArrow", true);
m_filterButton->setAutoRaise(true);
m_filterButton->setPopupMode(QToolButton::InstantPopup);
m_filterMenu = new QMenu(m_filterButton);
initializeFilterMenu();
m_filterButton->setMenu(m_filterMenu);
connect(m_expandAll, &QToolButton::clicked, m_treeView, &Utils::TreeView::expandAll);
connect(m_collapseAll, &QToolButton::clicked, m_treeView, &Utils::TreeView::collapseAll);
connect(m_filterMenu, &QMenu::triggered, this, &SquishOutputPane::onFilterMenuTriggered);
}
void SquishOutputPane::initializeFilterMenu()
{
QMap<Result::Type, QString> textAndType;
textAndType.insert(Result::Pass, tr("Pass"));
textAndType.insert(Result::Fail, tr("Fail"));
textAndType.insert(Result::ExpectedFail, tr("Expected Fail"));
textAndType.insert(Result::UnexpectedPass, tr("Unexpected Pass"));
textAndType.insert(Result::Warn, tr("Warning Messages"));
textAndType.insert(Result::Log, tr("Log Messages"));
const QList<Result::Type> types = textAndType.keys();
for (Result::Type type : types) {
QAction *action = new QAction(m_filterMenu);
action->setText(textAndType.value(type));
action->setCheckable(true);
action->setChecked(true);
action->setData(type);
m_filterMenu->addAction(action);
}
m_filterMenu->addSeparator();
QAction *action = new QAction(m_filterMenu);
action->setText(tr("Check All Filters"));
action->setCheckable(false);
m_filterMenu->addAction(action);
connect(action, &QAction::triggered, this, &SquishOutputPane::enableAllFiltersTriggered);
}
void SquishOutputPane::onItemActivated(const QModelIndex &idx)
{
if (!idx.isValid())
return;
const TestResult result = m_filterModel->testResult(idx);
if (!result.file().isEmpty())
Core::EditorManager::openEditorAt(
Utils::Link(Utils::FilePath::fromString(result.file()), result.line(), 0));
}
// TODO: this is currently a workaround - might vanish if a item delegate will be implemented
void SquishOutputPane::onSectionResized(int logicalIndex, int /*oldSize*/, int /*newSize*/)
{
// details column should have been modified by user, so no action, time stamp column is fixed
if (logicalIndex != 1) {
QHeaderView *header = m_treeView->header();
const int minimum = m_outputPane->width() - header->sectionSize(0) - header->sectionSize(2);
header->resizeSection(1, qMax(minimum, header->sectionSize(1)));
}
}
void SquishOutputPane::onFilterMenuTriggered(QAction *action)
{
m_filterModel->toggleResultType(Result::Type(action->data().toInt()));
navigateStateChanged();
}
void SquishOutputPane::enableAllFiltersTriggered()
{
const QList<QAction *> actions = m_filterMenu->actions();
for (QAction *action : actions)
action->setChecked(true);
m_filterModel->enableAllResultTypes();
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,107 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <coreplugin/ioutputpane.h>
QT_BEGIN_NAMESPACE
class QAction;
class QFrame;
class QLabel;
class QMenu;
class QModelIndex;
class QPlainTextEdit;
class QTabWidget;
class QToolButton;
QT_END_NAMESPACE
namespace Core { class IContext; }
namespace Utils { class TreeView; }
namespace Squish {
namespace Internal {
class TestResult;
class SquishResultItem;
class SquishResultModel;
class SquishResultFilterModel;
class SquishOutputPane : public Core::IOutputPane
{
Q_OBJECT
public:
static SquishOutputPane *instance();
// IOutputPane interface
QWidget *outputWidget(QWidget *parent) override;
QList<QWidget *> toolBarWidgets() const override;
QString displayName() const override;
int priorityInStatusBar() const override;
void clearContents() override;
void visibilityChanged(bool visible) override;
void setFocus() override;
bool hasFocus() const override;
bool canFocus() const override;
bool canNavigate() const override;
bool canNext() const override;
bool canPrevious() const override;
void goToNext() override;
void goToPrev() override;
public slots:
void addResultItem(SquishResultItem *item);
void addLogOutput(const QString &output);
void onTestRunFinished();
void clearOldResults();
private:
SquishOutputPane(QObject *parent = nullptr);
void createToolButtons();
void initializeFilterMenu();
void onItemActivated(const QModelIndex &idx);
void onSectionResized(int logicalIndex, int oldSize, int newSize);
void onFilterMenuTriggered(QAction *action);
void enableAllFiltersTriggered();
void updateSummaryLabel();
QTabWidget *m_outputPane;
Core::IContext *m_context;
QWidget *m_outputWidget;
QFrame *m_summaryWidget;
QLabel *m_summaryLabel;
Utils::TreeView *m_treeView;
SquishResultModel *m_model;
SquishResultFilterModel *m_filterModel;
QPlainTextEdit *m_runnerServerLog;
QToolButton *m_expandAll;
QToolButton *m_collapseAll;
QToolButton *m_filterButton;
QMenu *m_filterMenu;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,95 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishplugin.h"
#include "objectsmapeditor.h"
#include "squishnavigationwidget.h"
#include "squishoutputpane.h"
#include "squishsettings.h"
#include "squishsettingspage.h"
#include "squishtesttreemodel.h"
#include <coreplugin/icore.h>
#include <extensionsystem/pluginmanager.h>
#include <utils/mimetypes/mimedatabase.h>
#include <QtPlugin>
using namespace Squish::Internal;
using namespace Core;
static SquishPlugin *m_instance = nullptr;
SquishPlugin::SquishPlugin()
: m_squishSettings(new SquishSettings)
{
m_instance = this;
}
SquishPlugin::~SquishPlugin()
{
delete m_objectsMapEditorFactory;
delete m_navigationWidgetFactory;
delete m_settingsPage;
delete m_outputPane;
}
SquishPlugin *SquishPlugin::instance()
{
return m_instance;
}
QSharedPointer<SquishSettings> SquishPlugin::squishSettings() const
{
return m_squishSettings;
}
void SquishPlugin::initializeMenuEntries() {}
bool SquishPlugin::initialize(const QStringList &arguments, QString *errorString)
{
Q_UNUSED(arguments)
Q_UNUSED(errorString)
initializeMenuEntries();
m_squishSettings->fromSettings(ICore::settings());
m_treeModel = new SquishTestTreeModel(this);
m_settingsPage = new SquishSettingsPage(m_squishSettings);
m_navigationWidgetFactory = new SquishNavigationWidgetFactory;
m_outputPane = SquishOutputPane::instance();
m_objectsMapEditorFactory = new ObjectsMapEditorFactory;
return true;
}
void SquishPlugin::extensionsInitialized() {}
ExtensionSystem::IPlugin::ShutdownFlag SquishPlugin::aboutToShutdown()
{
return SynchronousShutdown;
}

View File

@@ -0,0 +1,74 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishplugin_global.h"
#include <extensionsystem/iplugin.h>
#include <QSharedPointer>
namespace Squish {
namespace Internal {
class ObjectsMapEditorFactory;
class SquishNavigationWidgetFactory;
class SquishOutputPane;
struct SquishSettings;
class SquishSettingsPage;
class SquishTestTreeModel;
class SquishPlugin : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Squish.json")
public:
SquishPlugin();
~SquishPlugin() override;
static SquishPlugin *instance();
QSharedPointer<SquishSettings> squishSettings() const;
bool initialize(const QStringList &arguments, QString *errorString) override;
void extensionsInitialized() override;
ShutdownFlag aboutToShutdown() override;
private:
void initializeMenuEntries();
SquishTestTreeModel *m_treeModel;
QSharedPointer<SquishSettings> m_squishSettings;
SquishSettingsPage *m_settingsPage = nullptr;
SquishNavigationWidgetFactory *m_navigationWidgetFactory = nullptr;
SquishOutputPane *m_outputPane = nullptr;
ObjectsMapEditorFactory *m_objectsMapEditorFactory = nullptr;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,38 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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.
**
****************************************************************************/
#ifndef SQUISHPLUGIN_GLOBAL_H
#define SQUISHPLUGIN_GLOBAL_H
#include <QtGlobal>
#if defined(SQUISH_LIBRARY)
# define SQUISHSHARED_EXPORT Q_DECL_EXPORT
#else
# define SQUISHSHARED_EXPORT Q_DECL_IMPORT
#endif
#endif // SQUISHPLUGIN_GLOBAL_H

View File

@@ -0,0 +1,197 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishresultmodel.h"
namespace Squish {
namespace Internal {
enum Role { Type = Qt::UserRole };
/************************** SquishResultItem ********************************/
SquishResultItem::SquishResultItem(const TestResult &result)
: m_testResult(result)
{}
QVariant SquishResultItem::data(int column, int role) const
{
switch (role) {
case Qt::ToolTipRole:
return m_testResult.text();
case Qt::DisplayRole:
switch (column) {
case 0:
return TestResult::typeToString(m_testResult.type());
case 1:
return m_testResult.text();
case 2:
return m_testResult.timeStamp();
}
break;
case Qt::ForegroundRole:
if (column == 0)
return TestResult::colorForType(m_testResult.type());
break;
case Type:
return m_testResult.type();
}
return QVariant();
}
/************************** SquishResultModel *******************************/
SquishResultModel::SquishResultModel(QObject *parent)
: Utils::TreeModel<>(parent)
, m_rootItem(new Utils::TreeItem)
{
setRootItem(m_rootItem);
setHeader(QStringList({tr("Result"), tr("Message"), tr("Time")}));
connect(this,
&QAbstractItemModel::rowsInserted,
this,
&SquishResultModel::updateResultTypeCount);
}
int SquishResultModel::resultTypeCount(Result::Type type)
{
return m_resultsCounter.value(type, 0);
}
void SquishResultModel::clearResults()
{
clear();
m_resultsCounter.clear();
emit resultTypeCountUpdated();
}
void SquishResultModel::expandVisibleRootItems()
{
m_rootItem->forChildrenAtLevel(1, [](Utils::TreeItem *item) { item->expand(); });
}
void SquishResultModel::updateResultTypeCount(const QModelIndex &parent, int first, int last)
{
bool countUpdated = false;
for (int i = first; i <= last; ++i) {
SquishResultItem *resultItem = static_cast<SquishResultItem *>(
parent.isValid() ? itemForIndex(parent)->childAt(i) : m_rootItem->childAt(i));
QHash<Result::Type, int> results;
++results[resultItem->result().type()];
resultItem->forAllChildren([&results](Utils::TreeItem *it) {
SquishResultItem *item = static_cast<SquishResultItem *>(it);
Result::Type type = item->result().type();
++results[type];
});
auto cend = results.constEnd();
for (auto pair = results.constBegin(); pair != cend; ++pair) {
Result::Type type = pair.key();
switch (type) {
case Result::Pass:
case Result::Fail:
case Result::ExpectedFail:
case Result::UnexpectedPass:
case Result::Warn:
case Result::Error:
case Result::Fatal:
if (int value = pair.value()) {
m_resultsCounter.insert(type, m_resultsCounter.value(type, 0) + value);
countUpdated = true;
}
break;
default:
break;
}
}
}
if (countUpdated)
emit resultTypeCountUpdated();
}
void SquishResultModel::addResultItem(SquishResultItem *item)
{
m_rootItem->appendChild(item);
}
/*********************** SquishResultFilerModel *****************************/
SquishResultFilterModel::SquishResultFilterModel(SquishResultModel *sourceModel, QObject *parent)
: QSortFilterProxyModel(parent)
, m_sourceModel(sourceModel)
{
setSourceModel(sourceModel);
enableAllResultTypes();
}
void SquishResultFilterModel::enableAllResultTypes()
{
m_enabled << Result::Log << Result::Pass << Result::Fail << Result::ExpectedFail
<< Result::UnexpectedPass << Result::Warn << Result::Error << Result::Fatal
<< Result::Start << Result::End << Result::Detail;
invalidateFilter();
}
void SquishResultFilterModel::toggleResultType(Result::Type type)
{
if (m_enabled.contains(type))
m_enabled.remove(type);
else
m_enabled.insert(type);
invalidateFilter();
}
void SquishResultFilterModel::clearResults()
{
m_sourceModel->clearResults();
}
bool SquishResultFilterModel::hasResults()
{
return m_sourceModel->hasResults();
}
TestResult SquishResultFilterModel::testResult(const QModelIndex &idx) const
{
if (auto item = static_cast<SquishResultItem *>(m_sourceModel->itemForIndex(mapToSource(idx))))
return item->result();
return TestResult();
}
bool SquishResultFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
const QModelIndex idx = m_sourceModel->index(sourceRow, 0, sourceParent);
if (!idx.isValid())
return false;
return m_enabled.contains(Result::Type(idx.data(Type).toInt()));
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,93 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "testresult.h"
#include <utils/treemodel.h>
#include <QSet>
#include <QSortFilterProxyModel>
namespace Squish {
namespace Internal {
class SquishResultItem : public Utils::TreeItem
{
public:
SquishResultItem(const TestResult &result);
QVariant data(int column, int role) const override;
TestResult result() const { return m_testResult; }
private:
TestResult m_testResult;
};
class SquishResultModel : public Utils::TreeModel<>
{
Q_OBJECT
public:
SquishResultModel(QObject *parent = nullptr);
bool hasResults() const { return m_rootItem ? m_rootItem->hasChildren() : false; }
int resultTypeCount(Result::Type type);
void clearResults();
void expandVisibleRootItems();
void updateResultTypeCount(const QModelIndex &parent, int first, int last);
void addResultItem(SquishResultItem *item);
signals:
void resultTypeCountUpdated();
private:
Utils::TreeItem *m_rootItem;
QHash<Result::Type, int> m_resultsCounter;
};
class SquishResultFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SquishResultFilterModel(SquishResultModel *sourceModel, QObject *parent = nullptr);
void enableAllResultTypes();
void toggleResultType(Result::Type type);
void clearResults();
bool hasResults();
TestResult testResult(const QModelIndex &idx) const;
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
SquishResultModel *m_sourceModel;
QSet<Result::Type> m_enabled;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,78 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishsettings.h"
#include <QSettings>
namespace Squish {
namespace Internal {
static const char group[] = "Squish";
static const char squishPathKey[] = "SquishPath";
static const char licensePathKey[] = "LicensePath";
static const char localKey[] = "Local";
static const char serverHostKey[] = "ServerHost";
static const char serverPortKey[] = "ServerPort";
static const char verboseKey[] = "Verbose";
void SquishSettings::toSettings(QSettings *s) const
{
s->beginGroup(group);
s->setValue(squishPathKey, squishPath.toString());
s->setValue(licensePathKey, licensePath.toString());
s->setValue(localKey, local);
s->setValue(serverHostKey, serverHost);
s->setValue(serverPortKey, serverPort);
s->setValue(verboseKey, verbose);
s->endGroup();
}
void SquishSettings::fromSettings(QSettings *s)
{
s->beginGroup(group);
squishPath = Utils::FilePath::fromVariant(s->value(squishPathKey));
licensePath = Utils::FilePath::fromVariant(s->value(licensePathKey));
local = s->value(localKey, true).toBool();
serverHost = s->value(serverHostKey, "localhost").toString();
serverPort = s->value(serverPortKey, 9999).toUInt();
verbose = s->value(verboseKey, false).toBool();
s->endGroup();
}
bool SquishSettings::operator==(const SquishSettings &other) const
{
return local == other.local && verbose == other.verbose && serverPort == other.serverPort
&& squishPath == other.squishPath && licensePath == other.licensePath
&& serverHost == other.serverHost;
}
bool SquishSettings::operator!=(const SquishSettings &other) const
{
return !(*this == other);
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,57 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/fileutils.h>
#include <QString>
#include <QtGlobal>
QT_BEGIN_NAMESPACE
class QSettings;
QT_END_NAMESPACE
namespace Squish {
namespace Internal {
struct SquishSettings
{
void toSettings(QSettings *s) const;
void fromSettings(QSettings *s);
bool operator==(const SquishSettings &other) const;
bool operator!=(const SquishSettings &other) const;
Utils::FilePath squishPath;
Utils::FilePath licensePath;
QString serverHost;
quint16 serverPort;
bool local;
bool verbose;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,108 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishsettingspage.h"
#include "squishconstants.h"
#include "squishsettings.h"
#include <coreplugin/icore.h>
#include <utils/theme/theme.h>
namespace Squish {
namespace Internal {
SquishSettingsWidget::SquishSettingsWidget(QWidget *parent)
: QWidget(parent)
{
m_ui.setupUi(this);
connect(m_ui.localCheckBox, &QCheckBox::toggled, this, &SquishSettingsWidget::onLocalToggled);
}
void SquishSettingsWidget::setSettings(const SquishSettings &settings)
{
m_ui.squishPathChooser->setFilePath(settings.squishPath);
m_ui.licensePathChooser->setFilePath(settings.licensePath);
m_ui.localCheckBox->setChecked(settings.local);
m_ui.serverHostLineEdit->setText(settings.serverHost);
m_ui.serverPortSpinBox->setValue(settings.serverPort);
m_ui.verboseCheckBox->setChecked(settings.verbose);
}
SquishSettings SquishSettingsWidget::settings() const
{
SquishSettings result;
result.squishPath = m_ui.squishPathChooser->filePath();
result.licensePath = m_ui.licensePathChooser->filePath();
result.local = m_ui.localCheckBox->checkState() == Qt::Checked;
result.serverHost = m_ui.serverHostLineEdit->text();
result.serverPort = m_ui.serverPortSpinBox->value();
result.verbose = m_ui.verboseCheckBox->checkState() == Qt::Checked;
return result;
}
void SquishSettingsWidget::onLocalToggled(bool checked)
{
m_ui.serverHostLineEdit->setEnabled(!checked);
m_ui.serverPortSpinBox->setEnabled(!checked);
}
SquishSettingsPage::SquishSettingsPage(const QSharedPointer<SquishSettings> &settings)
: m_settings(settings)
, m_widget(nullptr)
{
setId("A.Squish.General");
setDisplayName(tr("General"));
setCategory(Constants::SQUISH_SETTINGS_CATEGORY);
setDisplayCategory(tr("Squish"));
setCategoryIcon(Utils::Icon({{":/squish/images/settingscategory_squish.png",
Utils::Theme::PanelTextColorDark}},
Utils::Icon::Tint));
}
QWidget *SquishSettingsPage::widget()
{
if (!m_widget) {
m_widget = new SquishSettingsWidget;
m_widget->setSettings(*m_settings);
}
return m_widget;
}
void SquishSettingsPage::apply()
{
if (!m_widget) // page was not shown at all
return;
const SquishSettings newSettings = m_widget->settings();
if (newSettings != *m_settings) {
*m_settings = newSettings;
m_settings->toSettings(Core::ICore::settings());
}
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,72 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "ui_squishsettingspage.h"
#include <coreplugin/dialogs/ioptionspage.h>
#include <QPointer>
#include <QSharedPointer>
#include <QWidget>
namespace Squish {
namespace Internal {
struct SquishSettings;
class SquishSettingsWidget : public QWidget
{
Q_OBJECT
public:
explicit SquishSettingsWidget(QWidget *parent = nullptr);
void setSettings(const SquishSettings &settings);
SquishSettings settings() const;
private:
void onLocalToggled(bool checked);
Ui::SquishSettingsPage m_ui;
};
class SquishSettingsPage : public Core::IOptionsPage
{
Q_OBJECT
public:
explicit SquishSettingsPage(const QSharedPointer<SquishSettings> &settings);
QWidget *widget() override;
void apply() override;
void finish() override {}
private:
QSharedPointer<SquishSettings> m_settings;
QPointer<SquishSettingsWidget> m_widget;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Squish::Internal::SquishSettingsPage</class>
<widget class="QWidget" name="Squish::Internal::SquishSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>463</width>
<height>338</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="squishPathLabel">
<property name="text">
<string>Squish path:</string>
</property>
</widget>
</item>
<item row="0" column="2" colspan="5">
<widget class="Utils::PathChooser" name="squishPathChooser">
<property name="toolTip">
<string>Path to Squish installation directory.</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="licensePathLabel">
<property name="text">
<string>License path:</string>
</property>
</widget>
</item>
<item row="1" column="2" colspan="5">
<widget class="Utils::PathChooser" name="licensePathChooser">
<property name="toolTip">
<string>Path to directory containing Squish license file. You will only need this in special configurations like if you did not run the Squish setup tool.</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="localCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Local server</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLabel" name="serverHostLabel">
<property name="text">
<string>Server host:</string>
</property>
</widget>
</item>
<item row="2" column="4">
<widget class="QLineEdit" name="serverHostLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="5">
<widget class="QLabel" name="portLabel">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="2" column="6">
<widget class="QSpinBox" name="serverPortSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QCheckBox" name="verboseCheckBox">
<property name="text">
<string>Verbose log</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>289</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Utils::PathChooser</class>
<extends>QLineEdit</extends>
<header location="global">utils/pathchooser.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,433 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishtesttreemodel.h"
#include "squishfilehandler.h"
#include <utils/qtcassert.h>
#include <utils/utilsicons.h>
#include <QIcon>
namespace Squish {
namespace Internal {
/**************************** SquishTestTreeItem ***************************************/
SquishTestTreeItem::SquishTestTreeItem(const QString &displayName, Type type)
: m_displayName(displayName)
, m_type(type)
, m_checked(Qt::Checked)
{
switch (type) {
case Root:
m_flags = Qt::NoItemFlags;
break;
case SquishSuite:
m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserTristate
| Qt::ItemIsUserCheckable;
break;
case SquishTestCase:
m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
break;
case SquishSharedFile:
case SquishSharedFolder:
m_flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
break;
}
}
Qt::ItemFlags SquishTestTreeItem::flags(int /*column*/) const
{
return m_flags;
}
void SquishTestTreeItem::setFilePath(const QString &filePath)
{
m_filePath = filePath;
}
void SquishTestTreeItem::setParentName(const QString &parentName)
{
m_parentName = parentName;
}
void SquishTestTreeItem::setCheckState(Qt::CheckState state)
{
switch (m_type) {
case SquishTestCase:
m_checked = (state == Qt::Unchecked ? Qt::Unchecked : Qt::Checked);
static_cast<SquishTestTreeItem *>(parent())->revalidateCheckState();
break;
case SquishSuite:
m_checked = (state == Qt::Unchecked ? Qt::Unchecked : Qt::Checked);
forChildrenAtLevel(1, [this](TreeItem *item) {
static_cast<SquishTestTreeItem *>(item)->m_checked = m_checked;
});
break;
default:
break;
}
}
bool SquishTestTreeItem::modifyContent(const SquishTestTreeItem &other)
{
// modification applies only for items of the same type
if (m_type != other.m_type)
return false;
const bool modified = m_displayName != other.m_displayName || m_filePath != other.m_filePath
|| m_parentName != other.m_parentName;
m_displayName = other.m_displayName;
m_filePath = other.m_filePath;
m_parentName = other.m_parentName;
return modified;
}
void SquishTestTreeItem::revalidateCheckState()
{
if (childCount() == 0)
return;
bool foundChecked = false;
bool foundUnchecked = false;
forChildrenAtLevel(1, [&foundChecked, &foundUnchecked](const TreeItem *item) {
const SquishTestTreeItem *squishItem = static_cast<const SquishTestTreeItem *>(item);
foundChecked |= (squishItem->checkState() != Qt::Unchecked);
foundUnchecked |= (squishItem->checkState() == Qt::Unchecked);
});
if (foundChecked && foundUnchecked) {
m_checked = Qt::PartiallyChecked;
return;
}
m_checked = (foundUnchecked ? Qt::Unchecked : Qt::Checked);
}
/**************************** SquishTestTreeModel **************************************/
static SquishTestTreeModel *m_instance = nullptr;
SquishTestTreeModel::SquishTestTreeModel(QObject *parent)
: TreeModel<SquishTestTreeItem>(new SquishTestTreeItem(QString(), SquishTestTreeItem::Root),
parent)
, m_squishSharedFolders(new SquishTestTreeItem(tr("Shared Folders"), SquishTestTreeItem::Root))
, m_squishSuitesRoot(new SquishTestTreeItem(tr("Test Suites"), SquishTestTreeItem::Root))
, m_squishFileHandler(new SquishFileHandler(this))
{
rootItem()->appendChild(m_squishSharedFolders);
rootItem()->appendChild(m_squishSuitesRoot);
connect(m_squishFileHandler,
&SquishFileHandler::testTreeItemCreated,
this,
&SquishTestTreeModel::addTreeItem);
connect(m_squishFileHandler,
&SquishFileHandler::suiteTreeItemModified,
this,
&SquishTestTreeModel::onSuiteTreeItemModified);
connect(m_squishFileHandler,
&SquishFileHandler::suiteTreeItemRemoved,
this,
&SquishTestTreeModel::onSuiteTreeItemRemoved);
m_instance = this;
}
SquishTestTreeModel::~SquishTestTreeModel() {}
SquishTestTreeModel *SquishTestTreeModel::instance()
{
if (!m_instance)
m_instance = new SquishTestTreeModel;
return m_instance;
}
static QIcon treeIcon(SquishTestTreeItem::Type type, int column)
{
static QIcon icons[5] = {QIcon(),
Utils::Icons::OPENFILE.icon(),
QIcon(":/fancyactionbar/images/mode_Edit.png"),
Utils::Icons::OPENFILE.icon(),
QIcon(":/fancyactionbar/images/mode_Edit.png")};
if (column == 0)
return icons[type];
switch (type) {
case SquishTestTreeItem::SquishSuite:
if (column == 1)
return QIcon(":/squish/mages/play.png");
else if (column == 2)
return QIcon(":/squish/images/objectsmap.png");
break;
case SquishTestTreeItem::SquishTestCase:
if (column == 1)
return QIcon(":/squish/images/play.png");
else if (column == 2)
return QIcon(":/squish/images/record.png");
break;
default: // avoid warning of unhandled enum values
break;
}
return icons[0];
}
QVariant SquishTestTreeModel::data(const QModelIndex &idx, int role) const
{
if (!idx.isValid())
return QVariant();
if (SquishTestTreeItem *item = static_cast<SquishTestTreeItem *>(itemForIndex(idx))) {
const SquishTestTreeItem::Type type = item->type();
switch (role) {
case Qt::DisplayRole:
if (idx.column() > 0)
return QVariant();
switch (type) {
case SquishTestTreeItem::Root:
if (!item->hasChildren())
return tr("%1 (none)").arg(item->displayName());
return item->displayName();
case SquishTestTreeItem::SquishSharedFile:
case SquishTestTreeItem::SquishSharedFolder:
return item->displayName();
default: {
} // avoid warning regarding unhandled enum values
return item->displayName();
}
break;
case Qt::DecorationRole:
return treeIcon(type, idx.column());
case Qt::CheckStateRole:
if (idx.column() > 0)
return QVariant();
if (type == SquishTestTreeItem::SquishSuite
|| type == SquishTestTreeItem::SquishTestCase)
return item->checkState();
return QVariant();
case Qt::ToolTipRole:
if (type == SquishTestTreeItem::Root)
return QVariant();
if (item->displayName() == item->filePath())
return item->displayName();
return item->displayName().append('\n').append(item->filePath());
case LinkRole:
return item->filePath();
case TypeRole:
return type;
case DisplayNameRole:
return item->displayName();
}
}
return TreeModel::data(idx, role);
}
bool SquishTestTreeModel::setData(const QModelIndex &idx, const QVariant &data, int role)
{
if (!idx.isValid())
return false;
if (role == Qt::CheckStateRole) {
SquishTestTreeItem *item = static_cast<SquishTestTreeItem *>(itemForIndex(idx));
const SquishTestTreeItem::Type type = item->type();
if (type == SquishTestTreeItem::SquishSharedFolder
|| type == SquishTestTreeItem::SquishSharedFile)
return false;
Qt::CheckState old = item->checkState();
item->setCheckState((Qt::CheckState) data.toInt());
if (item->checkState() != old) {
switch (type) {
case SquishTestTreeItem::SquishSuite:
emit dataChanged(idx, idx);
if (rowCount(idx) > 0)
emit dataChanged(index(0, 0, idx), index(rowCount(idx), 0, idx));
break;
case SquishTestTreeItem::SquishTestCase:
emit dataChanged(idx, idx);
emit dataChanged(idx.parent(), idx.parent());
break;
default:
return false;
}
return true;
}
}
return false;
}
int SquishTestTreeModel::columnCount(const QModelIndex & /*idx*/) const
{
return COLUMN_COUNT;
}
void SquishTestTreeModel::addTreeItem(SquishTestTreeItem *item)
{
switch (item->type()) {
case SquishTestTreeItem::SquishSharedFolder:
m_squishSharedFolders->appendChild(item);
break;
case SquishTestTreeItem::SquishSuite:
m_squishSuitesRoot->appendChild(item);
break;
case SquishTestTreeItem::SquishTestCase: {
const QString folderName = item->parentName();
Utils::TreeItem *parent
= m_squishSuitesRoot->findChildAtLevel(1, [folderName](Utils::TreeItem *it) {
SquishTestTreeItem *squishItem = static_cast<SquishTestTreeItem *>(it);
return squishItem->displayName() == folderName;
});
if (parent)
parent->appendChild(item);
break;
}
case SquishTestTreeItem::SquishSharedFile: {
const QString folderName = item->parentName();
Utils::TreeItem *parent
= m_squishSharedFolders->findChildAtLevel(1, [folderName](Utils::TreeItem *it) {
SquishTestTreeItem *squishItem = static_cast<SquishTestTreeItem *>(it);
return squishItem->displayName() == folderName;
});
if (parent)
parent->appendChild(item);
break;
}
case SquishTestTreeItem::Root:
default:
qWarning("Not supposed to be used for Root items or unknown items.");
delete item;
break;
}
}
void SquishTestTreeModel::removeTreeItem(int row, const QModelIndex &parent)
{
if (!parent.isValid() || row >= rowCount(parent))
return;
Utils::TreeItem *toBeRemoved = itemForIndex(index(row, 0, parent));
takeItem(toBeRemoved);
delete toBeRemoved;
}
void SquishTestTreeModel::modifyTreeItem(int row,
const QModelIndex &parent,
const SquishTestTreeItem &modified)
{
if (!parent.isValid() || row >= rowCount(parent))
return;
QModelIndex childIndex = index(row, 0, parent);
SquishTestTreeItem *toBeModified = static_cast<SquishTestTreeItem *>(itemForIndex(childIndex));
if (toBeModified->modifyContent(modified))
emit dataChanged(childIndex, childIndex);
}
void SquishTestTreeModel::removeAllSharedFolders()
{
m_squishSharedFolders->removeChildren();
}
QStringList SquishTestTreeModel::getSelectedSquishTestCases(const QString &suiteConfPath) const
{
QStringList result;
const int count = m_squishSuitesRoot->childCount();
if (count) {
for (int row = 0; row < count; ++row) {
auto suiteItem = static_cast<SquishTestTreeItem *>(m_squishSuitesRoot->childAt(row));
if (suiteItem->filePath() == suiteConfPath) {
const int testCaseCount = suiteItem->childCount();
for (int caseRow = 0; caseRow < testCaseCount; ++caseRow) {
auto caseItem = static_cast<SquishTestTreeItem *>(suiteItem->childAt(caseRow));
if (caseItem->checkState() == Qt::Checked)
result.append(caseItem->displayName());
}
break;
}
}
}
return result;
}
SquishTestTreeItem *SquishTestTreeModel::findSuite(const QString &displayName) const
{
return findNonRootItem([&displayName](SquishTestTreeItem *item) {
return item->type() == SquishTestTreeItem::SquishSuite
&& item->displayName() == displayName;
});
}
void SquishTestTreeModel::onSuiteTreeItemRemoved(const QString &suiteName)
{
if (SquishTestTreeItem *suite = findSuite(suiteName)) {
const QModelIndex idx = suite->index();
removeTreeItem(idx.row(), idx.parent());
}
}
void SquishTestTreeModel::onSuiteTreeItemModified(SquishTestTreeItem *item, const QString &display)
{
if (SquishTestTreeItem *suite = findSuite(display)) {
const QModelIndex idx = suite->index();
modifyTreeItem(idx.row(), idx.parent(), *item);
}
// avoid leaking item even when it cannot be found
delete item;
}
/************************************** SquishTestTreeSortModel **********************************/
SquishTestTreeSortModel::SquishTestTreeSortModel(SquishTestTreeModel *sourceModel, QObject *parent)
: QSortFilterProxyModel(parent)
{
setSourceModel(sourceModel);
}
Utils::TreeItem *SquishTestTreeSortModel::itemFromIndex(const QModelIndex &idx) const
{
return static_cast<SquishTestTreeModel *>(sourceModel())->itemForIndex(mapToSource(idx));
}
bool SquishTestTreeSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
// root items keep intended order
const SquishTestTreeItem *leftItem = static_cast<SquishTestTreeItem *>(left.internalPointer());
if (leftItem->type() == SquishTestTreeItem::Root)
return left.row() > right.row();
const QString leftVal = left.data().toString();
const QString rightVal = right.data().toString();
return QString::compare(leftVal, rightVal, Qt::CaseInsensitive) > 0;
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,110 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/treemodel.h>
#include <QSortFilterProxyModel>
namespace {
enum ItemRole { LinkRole = Qt::UserRole + 2, TypeRole, DisplayNameRole };
}
namespace Squish {
namespace Internal {
class SquishFileHandler;
class SquishTestTreeItem : public Utils::TreeItem
{
public:
enum Type { Root, SquishSuite, SquishTestCase, SquishSharedFolder, SquishSharedFile };
SquishTestTreeItem(const QString &displayName, Type type);
~SquishTestTreeItem() override {}
Qt::ItemFlags flags(int column) const override;
QString displayName() const { return m_displayName; }
void setFilePath(const QString &filePath);
QString filePath() const { return m_filePath; }
void setParentName(const QString &parentName);
QString parentName() const { return m_parentName; }
Type type() const { return m_type; }
void setCheckState(Qt::CheckState state);
Qt::CheckState checkState() const { return m_checked; }
bool modifyContent(const SquishTestTreeItem &other);
private:
void revalidateCheckState();
QString m_displayName; // holds suite or test case name
QString m_filePath; // holds suite.conf path for suites, test.* for test cases
Type m_type;
QString m_parentName; // holds suite name for test cases, folder path for shared files
Qt::CheckState m_checked; // suites and test cases can have a check state
Qt::ItemFlags m_flags = Qt::NoItemFlags;
};
class SquishTestTreeModel : public Utils::TreeModel<SquishTestTreeItem>
{
public:
SquishTestTreeModel(QObject *parent = nullptr);
~SquishTestTreeModel() override;
static SquishTestTreeModel *instance();
static const int COLUMN_COUNT = 3;
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &idx, const QVariant &data, int role) override;
int columnCount(const QModelIndex &idx) const override;
void addTreeItem(SquishTestTreeItem *item);
void removeTreeItem(int row, const QModelIndex &parent);
void modifyTreeItem(int row, const QModelIndex &parent, const SquishTestTreeItem &modified);
void removeAllSharedFolders();
QStringList getSelectedSquishTestCases(const QString &suiteConfPath) const;
private:
SquishTestTreeItem *findSuite(const QString &displayName) const;
void onSuiteTreeItemRemoved(const QString &suiteName);
void onSuiteTreeItemModified(SquishTestTreeItem *item, const QString &display);
Utils::TreeItem *m_squishSharedFolders;
Utils::TreeItem *m_squishSuitesRoot;
SquishFileHandler *m_squishFileHandler;
};
class SquishTestTreeSortModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SquishTestTreeSortModel(SquishTestTreeModel *sourceModel, QObject *parent = nullptr);
Utils::TreeItem *itemFromIndex(const QModelIndex &idx) const;
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,132 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishtesttreeview.h"
#include "squishconstants.h"
#include "squishtesttreemodel.h"
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
namespace Squish {
namespace Internal {
SquishTestTreeView::SquishTestTreeView(QWidget *parent)
: Utils::NavigationTreeView(parent)
, m_context(new Core::IContext(this))
{
setExpandsOnDoubleClick(false);
m_context->setWidget(this);
m_context->setContext(Core::Context(Constants::SQUISH_CONTEXT));
Core::ICore::addContextObject(m_context);
}
void SquishTestTreeView::resizeEvent(QResizeEvent *event)
{
// override derived behavior of Utils::NavigationTreeView as we have more than 1 column
Utils::NavigationTreeView::resizeEvent(event);
}
void SquishTestTreeView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
const QModelIndex index = indexAt(event->pos());
if (index.isValid() && index.column() > 0 && index.column() < 3) {
int type = index.data(TypeRole).toInt();
if (type == SquishTestTreeItem::SquishSuite
|| type == SquishTestTreeItem::SquishTestCase) {
m_lastMousePressedIndex = index;
}
}
}
QTreeView::mousePressEvent(event);
}
void SquishTestTreeView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
const QModelIndex index = indexAt(event->pos());
if (index.isValid() && index == m_lastMousePressedIndex) {
int type = index.data(TypeRole).toInt();
if (type == SquishTestTreeItem::SquishSuite) {
if (index.column() == 1)
emit runTestSuite(index.data(DisplayNameRole).toString());
else if (index.column() == 2)
emit openObjectsMap(index.data(DisplayNameRole).toString());
} else {
const QModelIndex &suiteIndex = index.parent();
if (suiteIndex.isValid()) {
if (index.column() == 1) {
emit runTestCase(suiteIndex.data(DisplayNameRole).toString(),
index.data(DisplayNameRole).toString());
} else if (index.column() == 2) {
emit recordTestCase(suiteIndex.data(DisplayNameRole).toString(),
index.data(DisplayNameRole).toString());
}
}
}
}
}
QTreeView::mouseReleaseEvent(event);
}
/****************************** SquishTestTreeItemDelegate *************************************/
SquishTestTreeItemDelegate::SquishTestTreeItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{}
void SquishTestTreeItemDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &idx) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, idx);
// elide first column if necessary
if (idx.column() == 0)
opt.textElideMode = Qt::ElideMiddle;
// display disabled items as enabled
if (idx.flags() == Qt::NoItemFlags)
opt.palette.setColor(QPalette::Text, opt.palette.color(QPalette::Active, QPalette::Text));
QStyledItemDelegate::paint(painter, opt, idx);
}
QSize SquishTestTreeItemDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &idx) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, idx);
// elide first column if necessary
if (idx.column() == 0)
opt.textElideMode = Qt::ElideMiddle;
return QStyledItemDelegate::sizeHint(opt, idx);
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,71 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/navigationtreeview.h>
#include <QModelIndex>
#include <QStyledItemDelegate>
namespace Core { class IContext; }
namespace Squish {
namespace Internal {
class SquishTestTreeView : public Utils::NavigationTreeView
{
Q_OBJECT
public:
SquishTestTreeView(QWidget *parent = nullptr);
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
signals:
void runTestSuite(const QString &suiteName);
void runTestCase(const QString &suiteName, const QString &testCaseName);
void openObjectsMap(const QString &suiteName);
void recordTestCase(const QString &suiteName, const QString &testCaseName);
private:
Core::IContext *m_context;
QModelIndex m_lastMousePressedIndex;
};
class SquishTestTreeItemDelegate : public QStyledItemDelegate
{
public:
SquishTestTreeItemDelegate(QObject *parent = nullptr);
~SquishTestTreeItemDelegate() override {}
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &idx) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,743 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishtools.h"
#include "squishoutputpane.h"
#include "squishplugin.h"
#include "squishsettings.h"
#include "squishxmloutputhandler.h"
#include <QDebug> // TODO remove
#include <coreplugin/icore.h>
#include <utils/environment.h>
#include <utils/hostosinfo.h>
#include <utils/qtcassert.h>
#include <QApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileSystemWatcher>
#include <QMessageBox>
#include <QTimer>
#include <QWindow>
namespace Squish {
namespace Internal {
// make this configurable?
static const QString resultsDirectory = QFileInfo(QDir::home(), ".squishQC/Test Results")
.absoluteFilePath();
SquishTools::SquishTools(QObject *parent)
: QObject(parent)
, m_serverProcess(nullptr)
, m_runnerProcess(nullptr)
, m_serverPort(-1)
, m_request(None)
, m_state(Idle)
, m_currentResultsXML(nullptr)
, m_resultsFileWatcher(nullptr)
, m_testRunning(false)
, m_xmlOutputHandler(nullptr)
{
SquishOutputPane *outputPane = SquishOutputPane::instance();
connect(this,
&SquishTools::logOutputReceived,
outputPane,
&SquishOutputPane::addLogOutput,
Qt::QueuedConnection);
connect(this,
&SquishTools::squishTestRunStarted,
outputPane,
&SquishOutputPane::clearOldResults);
connect(this,
&SquishTools::squishTestRunFinished,
outputPane,
&SquishOutputPane::onTestRunFinished);
}
SquishTools::~SquishTools()
{
// TODO add confirmation dialog somewhere
if (m_runnerProcess) {
m_runnerProcess->terminate();
if (!m_runnerProcess->waitForFinished(5000))
m_runnerProcess->kill();
delete m_runnerProcess;
m_runnerProcess = nullptr;
}
if (m_serverProcess) {
m_serverProcess->terminate();
if (!m_serverProcess->waitForFinished(5000))
m_serverProcess->kill();
delete m_serverProcess;
m_serverProcess = nullptr;
}
delete m_xmlOutputHandler;
}
struct SquishToolsSettings
{
SquishToolsSettings() {}
QString squishPath;
QString serverPath = "squishserver";
QString runnerPath = "squishrunner";
bool isLocalServer = true;
bool verboseLog = false;
QString serverHost = "localhost";
int serverPort = 9999;
QString licenseKeyPath;
// populate members using current settings
void setup()
{
QSharedPointer<SquishSettings> squishSettings = SquishPlugin::instance()->squishSettings();
squishPath = squishSettings->squishPath.toString();
serverPath = Utils::HostOsInfo::withExecutableSuffix("squishserver");
runnerPath = Utils::HostOsInfo::withExecutableSuffix("squishrunner");
if (!squishPath.isEmpty()) {
const QDir squishBin(squishPath + QDir::separator() + "bin");
serverPath = QFileInfo(squishBin, serverPath).absoluteFilePath();
runnerPath = QFileInfo(squishBin, runnerPath).absoluteFilePath();
}
isLocalServer = squishSettings->local;
serverHost = squishSettings->serverHost;
serverPort = squishSettings->serverPort;
verboseLog = squishSettings->verbose;
licenseKeyPath = squishSettings->licensePath.toString();
}
};
void SquishTools::runTestCases(const QString &suitePath,
const QStringList &testCases,
const QStringList &additionalServerArgs,
const QStringList &additionalRunnerArgs)
{
if (m_state != Idle) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Error"),
tr("Squish Tools in unexpected state (%1).\n"
"Refusing to run a test case.")
.arg(m_state));
return;
}
// create test results directory (if necessary) and return on fail
if (!QDir().mkpath(resultsDirectory)) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Error"),
tr("Could not create test results folder. Canceling test run."));
return;
}
m_suitePath = suitePath;
m_testCases = testCases;
m_reportFiles.clear();
m_additionalServerArguments = additionalServerArgs;
const QString dateTimeString = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH-mm-ss");
m_currentResultsDirectory = QFileInfo(QDir(resultsDirectory), dateTimeString).absoluteFilePath();
m_additionalRunnerArguments = additionalRunnerArgs;
m_additionalRunnerArguments << "--interactive" << "--resultdir"
<< QDir::toNativeSeparators(m_currentResultsDirectory);
delete m_xmlOutputHandler;
m_xmlOutputHandler = new SquishXmlOutputHandler(this);
connect(this,
&SquishTools::resultOutputCreated,
m_xmlOutputHandler,
&SquishXmlOutputHandler::outputAvailable,
Qt::QueuedConnection);
m_testRunning = true;
emit squishTestRunStarted();
startSquishServer(RunTestRequested);
}
void SquishTools::setState(SquishTools::State state)
{
// TODO check whether state transition is legal
m_state = state;
switch (m_state) {
case Idle:
m_request = None;
m_suitePath = QString();
m_testCases.clear();
m_reportFiles.clear();
m_additionalRunnerArguments.clear();
m_additionalServerArguments.clear();
m_testRunning = false;
m_currentResultsDirectory.clear();
m_lastTopLevelWindows.clear();
break;
case ServerStarted:
if (m_request == RunTestRequested) {
startSquishRunner();
} else if (m_request == RecordTestRequested) {
} else if (m_request == RunnerQueryRequested) {
} else {
QTC_ASSERT(false, qDebug() << m_state << m_request);
}
break;
case ServerStartFailed:
m_state = Idle;
m_request = None;
if (m_testRunning) {
emit squishTestRunFinished();
m_testRunning = false;
}
restoreQtCreatorWindows();
break;
case ServerStopped:
m_state = Idle;
if (m_request == ServerStopRequested) {
m_request = None;
if (m_testRunning) {
emit squishTestRunFinished();
m_testRunning = false;
}
restoreQtCreatorWindows();
} else if (m_request == KillOldBeforeRunRunner) {
startSquishServer(RunTestRequested);
} else if (m_request == KillOldBeforeRecordRunner) {
startSquishServer(RecordTestRequested);
} else if (m_request == KillOldBeforeQueryRunner) {
startSquishServer(RunnerQueryRequested);
} else {
QTC_ASSERT(false, qDebug() << m_state << m_request);
}
break;
case ServerStopFailed:
if (m_serverProcess && m_serverProcess->state() != QProcess::NotRunning) {
m_serverProcess->terminate();
if (!m_serverProcess->waitForFinished(5000)) {
m_serverProcess->kill();
delete m_serverProcess;
m_serverProcess = nullptr;
}
}
m_state = Idle;
break;
case RunnerStartFailed:
case RunnerStopped:
if (m_testCases.isEmpty()) {
m_request = ServerStopRequested;
stopSquishServer();
QString error;
SquishXmlOutputHandler::mergeResultFiles(m_reportFiles,
m_currentResultsDirectory,
QDir(m_suitePath).dirName(),
&error);
if (!error.isEmpty())
QMessageBox::critical(Core::ICore::dialogParent(), tr("Error"), error);
logrotateTestResults();
} else {
m_xmlOutputHandler->clearForNextRun();
startSquishRunner();
}
break;
default:
break;
}
}
// make sure to execute setup() to populate with current settings before using it
static SquishToolsSettings toolsSettings;
void SquishTools::startSquishServer(Request request)
{
m_request = request;
if (m_serverProcess) {
if (QMessageBox::question(Core::ICore::dialogParent(),
tr("Squish Server Already Running"),
tr("There is still an old Squish server instance running.\n"
"This will cause problems later on.\n\n"
"If you continue, the old instance will be terminated.\n"
"Do you want to continue?"))
== QMessageBox::Yes) {
switch (m_request) {
case RunTestRequested:
m_request = KillOldBeforeRunRunner;
break;
case RecordTestRequested:
m_request = KillOldBeforeRecordRunner;
break;
case RunnerQueryRequested:
m_request = KillOldBeforeQueryRunner;
break;
default:
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Error"),
tr("Unexpected state or request while starting Squish "
"server. (state: %1, request: %2)")
.arg(m_state)
.arg(m_request));
}
stopSquishServer();
}
return;
}
toolsSettings.setup();
m_serverPort = -1;
const Utils::FilePath squishServer = Utils::Environment::systemEnvironment().searchInPath(
toolsSettings.serverPath);
if (squishServer.isEmpty()) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Squish Server Error"),
tr("\"%1\" could not be found or is not executable.\n"
"Check the settings.")
.arg(QDir::toNativeSeparators(toolsSettings.serverPath)));
setState(Idle);
return;
}
toolsSettings.serverPath = squishServer.toString();
if (true) // TODO squish setting of minimize QC on squish run/record
minimizeQtCreatorWindows();
else
m_lastTopLevelWindows.clear();
m_serverProcess = new QProcess;
m_serverProcess->setProgram(toolsSettings.serverPath);
QStringList arguments;
// TODO if isLocalServer is false we should start a squishserver on remote device
if (toolsSettings.isLocalServer)
arguments << "--local"; // for now - although Squish Docs say "don't use it"
else
arguments << "--port" << QString::number(toolsSettings.serverPort);
if (toolsSettings.verboseLog)
arguments << "--verbose";
m_serverProcess->setArguments(arguments);
m_serverProcess->setProcessEnvironment(squishEnvironment());
connect(m_serverProcess, &QProcess::readyReadStandardOutput, this, &SquishTools::onServerOutput);
connect(m_serverProcess,
&QProcess::readyReadStandardError,
this,
&SquishTools::onServerErrorOutput);
connect(m_serverProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
&SquishTools::onServerFinished);
setState(ServerStarting);
m_serverProcess->start();
if (!m_serverProcess->waitForStarted()) {
setState(ServerStartFailed);
qWarning() << "squishserver did not start within 30s";
}
}
void SquishTools::stopSquishServer()
{
if (m_serverProcess && m_serverPort > 0) {
QProcess serverKiller;
serverKiller.setProgram(m_serverProcess->program());
QStringList args;
args << "--stop" << "--port" << QString::number(m_serverPort);
serverKiller.setArguments(args);
serverKiller.setProcessEnvironment(m_serverProcess->processEnvironment());
serverKiller.start();
if (serverKiller.waitForStarted()) {
if (!serverKiller.waitForFinished()) {
qWarning() << "Could not shutdown server within 30s";
setState(ServerStopFailed);
}
} else {
qWarning() << "Could not shutdown server within 30s";
setState(ServerStopFailed);
}
} else {
qWarning() << "either no process running or port < 1?" << m_serverProcess << m_serverPort;
setState(ServerStopFailed);
}
}
void SquishTools::startSquishRunner()
{
if (!m_serverProcess) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("No Squish Server"),
tr("Squish server does not seem to be running.\n"
"(state: %1, request: %2)\n"
"Try again.")
.arg(m_state)
.arg(m_request));
setState(Idle);
return;
}
if (m_serverPort == -1) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("No Squish Server Port"),
tr("Failed to get the server port.\n"
"(state: %1, request: %2)\n"
"Try again.")
.arg(m_state)
.arg(m_request));
// setting state to ServerStartFailed will terminate/kill the current unusable server
setState(ServerStartFailed);
return;
}
if (m_runnerProcess) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Squish Runner Running"),
tr("Squish runner seems to be running already.\n"
"(state: %1, request: %2)\n"
"Wait until it has finished and try again.")
.arg(m_state)
.arg(m_request));
return;
}
const Utils::FilePath squishRunner = Utils::Environment::systemEnvironment().searchInPath(
toolsSettings.runnerPath);
if (squishRunner.isEmpty()) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Squish Runner Error"),
tr("\"%1\" could not be found or is not executable.\n"
"Check the settings.")
.arg(QDir::toNativeSeparators(toolsSettings.runnerPath)));
setState(RunnerStopped);
return;
}
toolsSettings.runnerPath = squishRunner.toString();
m_runnerProcess = new QProcess;
QStringList args;
args << m_additionalServerArguments;
if (!toolsSettings.isLocalServer)
args << "--host" << toolsSettings.serverHost;
args << "--port" << QString::number(m_serverPort);
args << "--debugLog" << "alpw"; // TODO make this configurable?
const QFileInfo testCasePath(QDir(m_suitePath), m_testCases.takeFirst());
args << "--testcase" << testCasePath.absoluteFilePath();
args << "--suitedir" << m_suitePath;
args << m_additionalRunnerArguments;
const QString caseReportFilePath = QFileInfo(QString::fromLatin1("%1/%2/%3/results.xml")
.arg(m_currentResultsDirectory,
QDir(m_suitePath).dirName(),
testCasePath.baseName()))
.absoluteFilePath();
m_reportFiles.append(caseReportFilePath);
args << "--reportgen"
<< QString::fromLatin1("xml2.2,%1").arg(caseReportFilePath);
m_runnerProcess->setProgram(toolsSettings.runnerPath);
m_runnerProcess->setArguments(args);
m_runnerProcess->setProcessEnvironment(squishEnvironment());
connect(m_runnerProcess,
&QProcess::readyReadStandardError,
this,
&SquishTools::onRunnerErrorOutput);
connect(m_runnerProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
&SquishTools::onRunnerFinished);
setState(RunnerStarting);
// set up the file system watcher for being able to read the results.xml file
m_resultsFileWatcher = new QFileSystemWatcher;
// on second run this directory exists and won't emit changes, so use the current subdirectory
if (QDir(m_currentResultsDirectory).exists())
m_resultsFileWatcher->addPath(m_currentResultsDirectory + QDir::separator()
+ QDir(m_suitePath).dirName());
else
m_resultsFileWatcher->addPath(QFileInfo(m_currentResultsDirectory).absolutePath());
connect(m_resultsFileWatcher,
&QFileSystemWatcher::directoryChanged,
this,
&SquishTools::onResultsDirChanged);
m_runnerProcess->start();
if (!m_runnerProcess->waitForStarted()) {
QMessageBox::critical(Core::ICore::dialogParent(),
tr("Squish Runner Error"),
tr("Squish runner failed to start within given timeframe."));
delete m_resultsFileWatcher;
m_resultsFileWatcher = nullptr;
setState(RunnerStartFailed);
return;
}
setState(RunnerStarted);
m_currentResultsXML = new QFile(caseReportFilePath);
}
QProcessEnvironment SquishTools::squishEnvironment()
{
Utils::Environment environment = Utils::Environment::systemEnvironment();
if (!toolsSettings.licenseKeyPath.isEmpty())
environment.prependOrSet("SQUISH_LICENSEKEY_DIR", toolsSettings.licenseKeyPath);
environment.prependOrSet("SQUISH_PREFIX", toolsSettings.squishPath);
return environment.toProcessEnvironment();
}
void SquishTools::onServerFinished(int, QProcess::ExitStatus)
{
delete m_serverProcess;
m_serverProcess = nullptr;
m_serverPort = -1;
setState(ServerStopped);
}
void SquishTools::onRunnerFinished(int, QProcess::ExitStatus)
{
delete m_runnerProcess;
m_runnerProcess = nullptr;
if (m_resultsFileWatcher) {
delete m_resultsFileWatcher;
m_resultsFileWatcher = nullptr;
}
if (m_currentResultsXML) {
// make sure results are being read if not done yet
if (m_currentResultsXML->exists() && !m_currentResultsXML->isOpen())
onResultsDirChanged(m_currentResultsXML->fileName());
if (m_currentResultsXML->isOpen())
m_currentResultsXML->close();
delete m_currentResultsXML;
m_currentResultsXML = nullptr;
}
setState(RunnerStopped);
}
void SquishTools::onServerOutput()
{
// output used for getting the port information of the current squishserver
const QByteArray output = m_serverProcess->readAllStandardOutput();
const QList<QByteArray> lines = output.split('\n');
for (const QByteArray &line : lines) {
const QByteArray trimmed = line.trimmed();
if (trimmed.isEmpty())
continue;
if (trimmed.startsWith("Port:")) {
if (m_serverPort == -1) {
bool ok;
int port = trimmed.mid(6).toInt(&ok);
if (ok) {
m_serverPort = port;
setState(ServerStarted);
} else {
qWarning() << "could not get port number" << trimmed.mid(6);
setState(ServerStartFailed);
}
} else {
qWarning() << "got a Port output - don't know why...";
}
}
emit logOutputReceived(QString("Server: ") + QLatin1String(trimmed));
}
}
void SquishTools::onServerErrorOutput()
{
// output that must be send to the Runner/Server Log
const QByteArray output = m_serverProcess->readAllStandardError();
const QList<QByteArray> lines = output.split('\n');
for (const QByteArray &line : lines) {
const QByteArray trimmed = line.trimmed();
if (!trimmed.isEmpty())
emit logOutputReceived(QString("Server: ") + QLatin1String(trimmed));
}
}
static char firstNonWhitespace(const QByteArray &text)
{
for (int i = 0, limit = text.size(); i < limit; ++i)
if (isspace(text.at(i)))
continue;
else
return text.at(i);
return 0;
}
static int positionAfterLastClosingTag(const QByteArray &text)
{
QList<QByteArray> possibleEndTags;
possibleEndTags << "</description>"
<< "</message>"
<< "</verification>"
<< "</result>"
<< "</test>"
<< "</prolog>"
<< "</epilog>"
<< "</SquishReport>";
int positionStart = text.lastIndexOf("</");
if (positionStart == -1)
return -1;
int positionEnd = text.indexOf('>', positionStart);
if (positionEnd == -1)
return -1;
QByteArray endTag = text.mid(positionStart, positionEnd + 1 - positionStart);
if (possibleEndTags.contains(endTag))
return positionEnd + 1;
return positionAfterLastClosingTag(text.mid(0, positionStart));
}
void SquishTools::onRunnerOutput()
{
// buffer for already read, but not processed content
static QByteArray buffer;
const qint64 currentSize = m_currentResultsXML->size();
if (currentSize <= m_readResultsCount)
return;
QByteArray output = m_currentResultsXML->read(currentSize - m_readResultsCount);
if (output.isEmpty())
return;
if (!buffer.isEmpty())
output.prepend(buffer);
// we might read only partial written stuff - so we have to figure out how much we can
// pass on for further processing and buffer the rest for the next reading
const int endTag = positionAfterLastClosingTag(output);
if (endTag < output.size()) {
buffer = output.mid(endTag);
output.truncate(endTag);
} else {
buffer.clear();
}
m_readResultsCount += output.size();
if (firstNonWhitespace(output) == '<') {
// output that must be used for the TestResultsPane
emit resultOutputCreated(output);
} else {
const QList<QByteArray> lines = output.split('\n');
for (const QByteArray &line : lines) {
const QByteArray trimmed = line.trimmed();
if (!trimmed.isEmpty())
emit logOutputReceived("Runner: " + QLatin1String(trimmed));
}
}
}
void SquishTools::onRunnerErrorOutput()
{
// output that must be send to the Runner/Server Log
const QByteArray output = m_runnerProcess->readAllStandardError();
const QList<QByteArray> lines = output.split('\n');
for (const QByteArray &line : lines) {
const QByteArray trimmed = line.trimmed();
if (!trimmed.isEmpty())
emit logOutputReceived("Runner: " + QLatin1String(trimmed));
}
}
void SquishTools::onResultsDirChanged(const QString &filePath)
{
if (!m_currentResultsXML)
return; // runner finished before, m_currentResultsXML deleted
if (m_currentResultsXML->exists()) {
delete m_resultsFileWatcher;
m_resultsFileWatcher = nullptr;
m_readResultsCount = 0;
if (m_currentResultsXML->open(QFile::ReadOnly)) {
m_resultsFileWatcher = new QFileSystemWatcher;
m_resultsFileWatcher->addPath(m_currentResultsXML->fileName());
connect(m_resultsFileWatcher,
&QFileSystemWatcher::fileChanged,
this,
&SquishTools::onRunnerOutput);
// squishrunner might have finished already, call once at least
onRunnerOutput();
} else {
// TODO set a flag to process results.xml as soon the complete test run has finished
qWarning() << "could not open results.xml although it exists" << filePath
<< m_currentResultsXML->error() << m_currentResultsXML->errorString();
}
} else {
disconnect(m_resultsFileWatcher);
// results.xml is created as soon some output has been opened - so try again in a second
QTimer::singleShot(1000, this, [this, filePath]() { onResultsDirChanged(filePath); });
}
}
void SquishTools::logrotateTestResults()
{
// make this configurable?
const int maxNumberOfTestResults = 10;
const QStringList existing = QDir(resultsDirectory)
.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
for (int i = 0, limit = existing.size() - maxNumberOfTestResults; i < limit; ++i) {
QDir current(resultsDirectory + QDir::separator() + existing.at(i));
if (!current.removeRecursively())
qWarning() << "could not remove" << current.absolutePath();
}
}
void SquishTools::minimizeQtCreatorWindows()
{
m_lastTopLevelWindows = QApplication::topLevelWindows();
QWindowList toBeRemoved;
for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
if (window->isVisible())
window->showMinimized();
else
toBeRemoved.append(window);
}
for (QWindow *window : qAsConst(toBeRemoved))
m_lastTopLevelWindows.removeOne(window);
}
void SquishTools::restoreQtCreatorWindows()
{
for (QWindow *window : qAsConst(m_lastTopLevelWindows)) {
window->requestActivate();
window->showNormal();
}
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,124 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QObject>
#include <QProcess>
#include <QStringList>
#include <QWindowList>
QT_BEGIN_NAMESPACE
class QFile;
class QFileSystemWatcher;
QT_END_NAMESPACE
namespace Squish {
namespace Internal {
class SquishXmlOutputHandler;
class SquishTools : public QObject
{
Q_OBJECT
public:
explicit SquishTools(QObject *parent = nullptr);
~SquishTools() override;
enum State {
Idle,
ServerStarting,
ServerStarted,
ServerStartFailed,
ServerStopped,
ServerStopFailed,
RunnerStarting,
RunnerStarted,
RunnerStartFailed,
RunnerStopped
};
State state() const { return m_state; }
void runTestCases(const QString &suitePath,
const QStringList &testCases = QStringList(),
const QStringList &additionalServerArgs = QStringList(),
const QStringList &additionalRunnerArgs = QStringList());
signals:
void logOutputReceived(const QString &output);
void squishTestRunStarted();
void squishTestRunFinished();
void resultOutputCreated(const QByteArray &output);
private:
enum Request {
None,
ServerStopRequested,
ServerQueryRequested,
RunnerQueryRequested,
RunTestRequested,
RecordTestRequested,
KillOldBeforeRunRunner,
KillOldBeforeRecordRunner,
KillOldBeforeQueryRunner
};
void setState(State state);
void startSquishServer(Request request);
void stopSquishServer();
void startSquishRunner();
static QProcessEnvironment squishEnvironment();
Q_SLOT void onServerFinished(int exitCode, QProcess::ExitStatus status = QProcess::NormalExit);
Q_SLOT void onRunnerFinished(int exitCode, QProcess::ExitStatus status = QProcess::NormalExit);
void onServerOutput();
void onServerErrorOutput();
void onRunnerOutput();
void onRunnerErrorOutput();
void onResultsDirChanged(const QString &filePath);
static void logrotateTestResults();
void minimizeQtCreatorWindows();
void restoreQtCreatorWindows();
QProcess *m_serverProcess;
QProcess *m_runnerProcess;
int m_serverPort;
QString m_serverHost;
Request m_request;
State m_state;
QString m_suitePath;
QStringList m_testCases;
QStringList m_reportFiles;
QString m_currentResultsDirectory;
QFile *m_currentResultsXML;
QFileSystemWatcher *m_resultsFileWatcher;
QStringList m_additionalServerArguments;
QStringList m_additionalRunnerArguments;
QWindowList m_lastTopLevelWindows;
bool m_testRunning;
qint64 m_readResultsCount;
SquishXmlOutputHandler *m_xmlOutputHandler;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,100 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishutils.h"
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QSettings>
#include <QString>
namespace Squish {
namespace Internal {
const static char squishLanguageKey[] = "LANGUAGE";
const static char squishTestCasesKey[] = "TEST_CASES";
const static char objectsMapKey[] = "OBJECTMAP";
QStringList SquishUtils::validTestCases(const QString &baseDirectory)
{
QStringList validCases;
QDir subDir(baseDirectory);
QFileInfo suiteConf(subDir, "suite.conf");
if (suiteConf.exists()) {
QVariantMap conf = readSuiteConf(suiteConf.absoluteFilePath());
QString extension = extensionForLanguage(conf.value(squishLanguageKey).toString());
QStringList cases = conf.value(squishTestCasesKey)
.toString()
.split(QRegularExpression("\\s+"));
for (const QString &testCase : qAsConst(cases)) {
QFileInfo testCaseDirInfo(subDir, testCase);
if (testCaseDirInfo.isDir()) {
QFileInfo testCaseTestInfo(testCaseDirInfo.filePath(), "test" + extension);
if (testCaseTestInfo.isFile())
validCases.append(testCaseTestInfo.absoluteFilePath());
}
}
}
return validCases;
}
QVariantMap SquishUtils::readSuiteConf(const QString &suiteConfPath)
{
const QSettings suiteConf(suiteConfPath, QSettings::IniFormat);
QVariantMap result;
// TODO get all information - actually only the information needed now is fetched
result.insert(squishLanguageKey, suiteConf.value(squishLanguageKey));
result.insert(squishTestCasesKey, suiteConf.value(squishTestCasesKey));
return result;
}
QString SquishUtils::objectsMapPath(const QString &suitePath)
{
const QString suiteDir = QFileInfo(suitePath).absolutePath();
const QSettings suiteConf(suitePath, QSettings::IniFormat);
const QString objMapPath = suiteConf.value(objectsMapKey).toString();
return QFileInfo(suiteDir, objMapPath).canonicalFilePath();
}
QString SquishUtils::extensionForLanguage(const QString &language)
{
if (language == "Python")
return ".py";
if (language == "Perl")
return ".pl";
if (language == "JavaScript")
return ".js";
if (language == "Ruby")
return ".rb";
if (language == "Tcl")
return ".tcl";
return QString(); // better return an invalid extension?
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,47 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QString>
#include <QVariantMap>
namespace Squish {
namespace Internal {
class SquishUtils
{
public:
static QStringList validTestCases(const QString &baseDirectory);
static QVariantMap readSuiteConf(const QString &suiteConfPath);
static QString objectsMapPath(const QString &suitePath);
static QString extensionForLanguage(const QString &language);
private:
SquishUtils() {}
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,323 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "squishxmloutputhandler.h"
#include "squishoutputpane.h"
#include "squishresultmodel.h"
#include <utils/qtcassert.h>
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QXmlStreamWriter>
namespace Squish {
namespace Internal {
SquishXmlOutputHandler::SquishXmlOutputHandler(QObject *parent)
: QObject(parent)
{
connect(this,
&SquishXmlOutputHandler::resultItemCreated,
SquishOutputPane::instance(),
&SquishOutputPane::addResultItem,
Qt::QueuedConnection);
}
void SquishXmlOutputHandler::clearForNextRun()
{
m_xmlReader.clear();
}
void SquishXmlOutputHandler::mergeResultFiles(const QStringList &reportFiles,
const QString &resultsDirectory,
const QString &suiteName,
QString *error)
{
QFile resultsXML(QString::fromLatin1("%1/results.xml").arg(resultsDirectory));
if (resultsXML.exists()) {
if (error)
*error = tr("Could not merge results into single results.xml.\n"
"Destination file \"%1\" already exists.")
.arg(resultsXML.fileName());
return;
}
if (!resultsXML.open(QFile::WriteOnly)) {
if (error)
*error = tr("Could not merge results into single results.xml.\n"
"Failed to open file \"%1\"")
.arg(resultsXML.fileName());
return;
}
QXmlStreamWriter xmlWriter(&resultsXML);
xmlWriter.writeStartDocument("1.0");
bool isFirstReport = true;
bool isFirstTest = true;
QString lastEpilogTime;
for (const QString &caseResult : reportFiles) {
QFile currentResultsFile(caseResult);
if (!currentResultsFile.exists())
continue;
if (!currentResultsFile.open(QFile::ReadOnly))
continue;
QXmlStreamReader reader(&currentResultsFile);
while (!reader.atEnd()) {
QXmlStreamReader::TokenType type = reader.readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString tagName = reader.name().toString();
// SquishReport of the first results.xml will be taken as is and as this is a
// merged results.xml we add another test tag holding the suite's name
if (tagName == "SquishReport") {
if (isFirstReport) {
xmlWriter.writeStartElement(tagName);
xmlWriter.writeAttributes(reader.attributes());
xmlWriter.writeStartElement("test");
xmlWriter.writeAttribute("name", suiteName);
isFirstReport = false;
}
break;
}
if (isFirstTest && tagName == "test") {
// the prolog tag of the first results.xml holds the start time of the suite
// we already wrote the test tag for the suite, but haven't added the start
// time as we didn't know about it, so store information of the current test
// tag (case name), read ahead (prolog tag), write prolog (suite's test tag)
// and finally write test tag (case name) - the prolog tag (for test case)
// will be written outside the if
const QXmlStreamAttributes testAttributes = reader.attributes();
QXmlStreamReader::TokenType token;
while (!reader.atEnd()) {
token = reader.readNext();
if (token != QXmlStreamReader::Characters)
break;
}
const QString prolog = reader.name().toString();
QTC_ASSERT(token == QXmlStreamReader::StartElement
&& prolog == "prolog",
if (error) *error = tr("Error while parsing first test result.");
return );
xmlWriter.writeStartElement(prolog);
xmlWriter.writeAttributes(reader.attributes());
xmlWriter.writeEndElement();
xmlWriter.writeStartElement("test");
xmlWriter.writeAttributes(testAttributes);
isFirstTest = false;
} else if (tagName == "epilog") {
lastEpilogTime = reader.attributes().value("time").toString();
}
xmlWriter.writeCurrentToken(reader);
break;
}
case QXmlStreamReader::EndElement:
if (reader.name() != QLatin1String("SquishReport"))
xmlWriter.writeCurrentToken(reader);
break;
case QXmlStreamReader::Characters:
xmlWriter.writeCurrentToken(reader);
break;
// ignore the rest
default:
break;
}
}
currentResultsFile.close();
}
if (!lastEpilogTime.isEmpty()) {
xmlWriter.writeStartElement("epilog");
xmlWriter.writeAttribute("time", lastEpilogTime);
xmlWriter.writeEndElement();
}
xmlWriter.writeEndDocument();
}
Result::Type resultFromString(const QString &type)
{
if (type == "DETAILED")
return Result::Detail;
if (type == "LOG")
return Result::Log;
if (type == "PASS")
return Result::Pass;
if (type == "FAIL")
return Result::Fail;
if (type == "WARNING")
return Result::Warn;
if (type == "XFAIL")
return Result::ExpectedFail;
if (type == "XPASS")
return Result::UnexpectedPass;
if (type == "FATAL")
return Result::Fatal;
if (type == "ERROR")
return Result::Error;
return Result::Log;
}
// this method uses the XML reader to parse output of the Squish results.xml and put it into an
// item that can be used to display inside the test results pane
// TODO: support Squish report 3.x as well
void SquishXmlOutputHandler::outputAvailable(const QByteArray &output)
{
static SquishResultItem *testCaseRootItem;
static QString name;
static QString details;
static QString logDetails;
static QStringList logDetailsList;
static QString time;
static QString file;
static Result::Type type;
static int line = 0;
static bool prepend = false;
m_xmlReader.addData(output);
while (!m_xmlReader.atEnd()) {
QXmlStreamReader::TokenType tokenType = m_xmlReader.readNext();
switch (tokenType) {
case QXmlStreamReader::StartDocument:
case QXmlStreamReader::EndDocument:
break;
case QXmlStreamReader::StartElement: {
const QString currentName = m_xmlReader.name().toString();
// tags verification, message, epilog and test will start a new entry, so, reset values
if (currentName == "verification"
|| currentName == "message"
|| currentName == "epilog"
|| currentName == "test") {
name = currentName;
details.clear();
logDetails.clear();
logDetailsList.clear();
time.clear();
file.clear();
line = 0;
type = Result::Log;
if (currentName == "test")
testCaseRootItem = nullptr;
} else if (currentName == "result") {
// result tag won't add another entry, but gives more information on enclosing tag
name = currentName;
}
// description tag could provide information that must be prepended to the former entry
if (currentName == "description") {
prepend = (name == "result" && m_xmlReader.attributes().isEmpty());
} else {
const QXmlStreamAttributes attributes = m_xmlReader.attributes();
for (const QXmlStreamAttribute &att : attributes) {
const QString attributeName = att.name().toString();
if (attributeName == "time")
time = QDateTime::fromString(att.value().toString(), Qt::ISODate)
.toString("MMM dd, yyyy h:mm:ss AP");
else if (attributeName == "file")
file = att.value().toString();
else if (attributeName == "line")
line = att.value().toInt();
else if (attributeName == "type")
type = resultFromString(att.value().toString());
else if (attributeName == "name")
logDetails = att.value().toString();
}
}
// handle prolog (test) elements already within the start tag
if (currentName == "prolog") {
TestResult result(Result::Start, logDetails, time);
result.setFile(file);
result.setLine(line);
testCaseRootItem = new SquishResultItem(result);
emit resultItemCreated(testCaseRootItem);
}
break;
}
case QXmlStreamReader::EndElement: {
const QString currentName = m_xmlReader.name().toString();
// description and result tags are handled differently, test tags are handled by
// prolog tag (which is handled in QXmlStreamReader::StartElement already),
// SquishReport tags will be ignored completely
if (currentName == "epilog") {
QTC_ASSERT(testCaseRootItem, break);
TestResult result(Result::End, QString(), time);
SquishResultItem *item = new SquishResultItem(result);
testCaseRootItem->appendChild(item);
} else if (currentName == "description") {
if (!prepend && !details.trimmed().isEmpty()) {
logDetailsList.append(details.trimmed());
details.clear();
}
} else if (currentName != "prolog"
&& currentName != "test"
&& currentName != "result"
&& currentName != "SquishReport") {
TestResult result(type, logDetails, time);
if (logDetails.isEmpty() && !logDetailsList.isEmpty())
result.setText(logDetailsList.takeFirst());
result.setFile(file);
result.setLine(line);
SquishResultItem *item = new SquishResultItem(result);
if (!logDetailsList.isEmpty()) {
for (const QString &detail : qAsConst(logDetailsList)) {
TestResult childResult(Result::Detail, detail);
SquishResultItem *childItem = new SquishResultItem(childResult);
item->appendChild(childItem);
}
}
testCaseRootItem->appendChild(item);
}
break;
}
case QXmlStreamReader::Characters: {
QString text = m_xmlReader.text().toString();
if (m_xmlReader.isCDATA() || !text.trimmed().isEmpty()) {
if (!m_xmlReader.isCDATA())
text = text.trimmed();
if (prepend) {
if (!logDetails.isEmpty() && (text == "Verified" || text == "Not Verified"))
logDetails.prepend(text + ": ");
else
logDetails = text;
} else {
details.append(text).append('\n');
}
}
break;
}
default:
break;
}
}
if (m_xmlReader.hasError()) {
// kind of expected as we get the output piece by piece
if (m_xmlReader.error() != QXmlStreamReader::PrematureEndOfDocumentError)
qWarning() << m_xmlReader.error() << m_xmlReader.errorString();
}
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,61 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "testresult.h"
#include <QObject>
#include <QXmlStreamReader>
namespace Squish {
namespace Internal {
class SquishResultItem;
class SquishXmlOutputHandler : public QObject
{
Q_OBJECT
public:
explicit SquishXmlOutputHandler(QObject *parent = nullptr);
void clearForNextRun();
static void mergeResultFiles(const QStringList &reportFiles,
const QString &resultsDirectory,
const QString &suiteName,
QString *error = nullptr);
signals:
void resultItemCreated(SquishResultItem *resultItem);
public slots:
void outputAvailable(const QByteArray &output);
private:
QXmlStreamReader m_xmlReader;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,90 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "symbolnameitemdelegate.h"
#include "objectsmaptreeitem.h"
#include <utils/treemodel.h>
namespace Squish {
namespace Internal {
/********************************** SymbolNameItemDelegate ************************************/
SymbolNameItemDelegate::SymbolNameItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{}
QWidget *SymbolNameItemDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &,
const QModelIndex &index) const
{
if (auto filterModel = qobject_cast<const ObjectsMapSortFilterModel *>(index.model()))
if (auto treeModel = qobject_cast<ObjectsMapModel *>(filterModel->sourceModel()))
return new ValidatingContainerNameLineEdit(treeModel->allSymbolicNames(), parent);
return new ValidatingContainerNameLineEdit(QStringList(), parent);
}
void SymbolNameItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
if (auto lineEdit = qobject_cast<Utils::FancyLineEdit *>(editor))
lineEdit->setText(index.data().toString());
}
void SymbolNameItemDelegate::setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const
{
if (auto edit = qobject_cast<ValidatingContainerNameLineEdit *>(editor)) {
if (!edit->isValid())
return;
}
QStyledItemDelegate::setModelData(editor, model, index);
}
/******************************* ValidatingContainerNameEdit **********************************/
ValidatingContainerNameLineEdit::ValidatingContainerNameLineEdit(const QStringList &forbidden,
QWidget *parent)
: Utils::FancyLineEdit(parent)
, m_forbidden(forbidden)
{
setValidationFunction([this](FancyLineEdit *edit, QString * /*errorMessage*/) {
if (!edit)
return false;
const QString &value = edit->text();
if (value.isEmpty())
return false;
const QString realName = value.at(0) == ObjectsMapTreeItem::COLON
? value
: ObjectsMapTreeItem::COLON + value;
return !m_forbidden.contains(realName);
});
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,61 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <utils/fancylineedit.h>
#include <QStyledItemDelegate>
namespace Squish {
namespace Internal {
class SymbolNameItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SymbolNameItemDelegate(QObject *parent = nullptr);
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const override;
};
class ValidatingContainerNameLineEdit : public Utils::FancyLineEdit
{
Q_OBJECT
public:
ValidatingContainerNameLineEdit(const QStringList &forbidden, QWidget *parent = nullptr);
private:
QStringList m_forbidden;
};
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,97 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 "testresult.h"
#include <utils/theme/theme.h>
namespace Squish {
namespace Internal {
TestResult::TestResult(Result::Type type, const QString &text, const QString &timeStamp)
: m_type(type)
, m_text(text)
, m_timeStamp(timeStamp)
, m_line(-1)
{}
QString TestResult::typeToString(Result::Type type)
{
switch (type) {
case Result::Log:
return "Log";
case Result::Pass:
return "Pass";
case Result::Fail:
return "Fail";
case Result::ExpectedFail:
return "Expected Fail";
case Result::UnexpectedPass:
return "Unexpected Pass";
case Result::Warn:
return "Warning";
case Result::Error:
return "Error";
case Result::Fatal:
return "Fatal";
case Result::Detail:
return "Detail";
case Result::Start:
return "Test Start";
case Result::End:
return "Test Finished";
}
return "UNKNOWN";
}
QColor TestResult::colorForType(Result::Type type)
{
Utils::Theme *creatorTheme = Utils::creatorTheme();
switch (type) {
case Result::Start:
case Result::Log:
case Result::Detail:
case Result::End:
return creatorTheme->color(Utils::Theme::OutputPanes_StdOutTextColor);
case Result::Pass:
return creatorTheme->color(Utils::Theme::OutputPanes_TestPassTextColor);
case Result::Fail:
case Result::Error:
return creatorTheme->color(Utils::Theme::OutputPanes_TestFailTextColor);
case Result::ExpectedFail:
return creatorTheme->color(Utils::Theme::OutputPanes_TestXFailTextColor);
case Result::UnexpectedPass:
return creatorTheme->color(Utils::Theme::OutputPanes_TestXPassTextColor);
case Result::Warn:
return creatorTheme->color(Utils::Theme::OutputPanes_TestWarnTextColor);
case Result::Fatal:
return creatorTheme->color(Utils::Theme::OutputPanes_TestFatalTextColor);
}
return creatorTheme->color(Utils::Theme::OutputPanes_StdOutTextColor);
}
} // namespace Internal
} // namespace Squish

View File

@@ -0,0 +1,71 @@
/****************************************************************************
**
** Copyright (C) 2022 The Qt Company Ltd
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator Squish plugin.
**
** 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 <QColor>
#include <QMetaType>
#include <QString>
namespace Squish {
namespace Internal {
namespace Result {
enum Type { Log, Pass, Fail, ExpectedFail, UnexpectedPass, Warn, Error, Fatal, Detail, Start, End };
}
class TestResult
{
public:
TestResult(Result::Type type = Result::Log,
const QString &text = QString(),
const QString &timeStamp = QString());
Result::Type type() const { return m_type; }
QString text() const { return m_text; }
QString timeStamp() const { return m_timeStamp; }
void setText(const QString &text) { m_text = text; }
void setFile(const QString &file) { m_file = file; }
void setLine(int line) { m_line = line; }
QString file() const { return m_file; }
int line() const { return m_line; }
static QString typeToString(Result::Type type);
static QColor colorForType(Result::Type type);
private:
Result::Type m_type;
QString m_text;
QString m_timeStamp;
QString m_file;
int m_line;
};
} // namespace Internal
} // namespace Squish
Q_DECLARE_METATYPE(Squish::Internal::TestResult)
Q_DECLARE_METATYPE(Squish::Internal::Result::Type)

View File

@@ -107,7 +107,7 @@ std::unique_ptr<QSettings> makeUserFeedbackSettings()
{
QStringList domain = QCoreApplication::organizationDomain().split(QLatin1Char('.'));
std::reverse(domain.begin(), domain.end());
QString productId = domain.join(QLatin1String("."));
QString productId = domain.join('.');
if (!productId.isEmpty())
productId += ".";
productId += QCoreApplication::applicationName();