forked from qt-creator/qt-creator
QmlDesigner: Make the material editor preview resizable
* Also the ui for the material editor preview is changed. Task-number: QDS-12928 Change-Id: I37cdb5f5f0b701fd0eb9b00f837a7e5738829ea3 Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io> Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
@@ -8,7 +8,7 @@ import HelperWidgets as HelperWidgets
|
||||
import StudioControls as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
|
||||
Column {
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string previewEnv
|
||||
@@ -18,7 +18,7 @@ Column {
|
||||
|
||||
property StudioTheme.ControlStyle buttonStyle: StudioTheme.ViewBarButtonStyle {
|
||||
//This is how you can override stuff from the control styles
|
||||
controlSize: Qt.size(previewOptions.width, previewOptions.width)
|
||||
controlSize: Qt.size(optionsToolbar.height, optionsToolbar.height)
|
||||
baseIconFontSize: StudioTheme.Values.bigIconFontSize
|
||||
}
|
||||
|
||||
@@ -118,16 +118,16 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width
|
||||
height: previewRect.height
|
||||
Row {
|
||||
id: optionsToolbar
|
||||
|
||||
Layout.preferredHeight: 40
|
||||
Layout.fillWidth: true
|
||||
|
||||
leftPadding: root.__horizontalSpacing
|
||||
StudioControls.AbstractButton {
|
||||
id: pinButton
|
||||
|
||||
x: root.__horizontalSpacing
|
||||
|
||||
style: root.buttonStyle
|
||||
iconSize: StudioTheme.Values.bigFont
|
||||
buttonIcon: pinButton.checked ? StudioTheme.Constants.pin : StudioTheme.Constants.unpin
|
||||
@@ -136,56 +136,53 @@ Column {
|
||||
onCheckedChanged: itemPane.headerDocked = pinButton.checked
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: previewRect
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 152
|
||||
height: 152
|
||||
color: "#000000"
|
||||
|
||||
Image {
|
||||
id: materialPreview
|
||||
width: 150
|
||||
height: 150
|
||||
anchors.centerIn: parent
|
||||
source: "image://materialEditor/preview"
|
||||
cache: false
|
||||
smooth: true
|
||||
}
|
||||
HelperWidgets.AbstractButton {
|
||||
style: root.buttonStyle
|
||||
buttonIcon: StudioTheme.Constants.textures_medium
|
||||
tooltip: qsTr("Select preview environment.")
|
||||
onClicked: envMenu.popup()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: previewOptions
|
||||
width: 40
|
||||
height: previewRect.height
|
||||
anchors.top: previewRect.top
|
||||
anchors.left: previewRect.right
|
||||
anchors.leftMargin: root.__horizontalSpacing
|
||||
HelperWidgets.AbstractButton {
|
||||
style: root.buttonStyle
|
||||
buttonIcon: StudioTheme.Constants.cube_medium
|
||||
tooltip: qsTr("Select preview model.")
|
||||
onClicked: modelMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Rectangle {
|
||||
id: previewRect
|
||||
|
||||
HelperWidgets.AbstractButton {
|
||||
style: root.buttonStyle
|
||||
buttonIcon: StudioTheme.Constants.textures_medium
|
||||
tooltip: qsTr("Select preview environment.")
|
||||
onClicked: envMenu.popup()
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 152
|
||||
implicitHeight: materialPreview.height
|
||||
|
||||
HelperWidgets.AbstractButton {
|
||||
style: root.buttonStyle
|
||||
buttonIcon: StudioTheme.Constants.cube_medium
|
||||
tooltip: qsTr("Select preview model.")
|
||||
onClicked: modelMenu.popup()
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
color: "#000000"
|
||||
|
||||
Image {
|
||||
id: materialPreview
|
||||
|
||||
width: root.width
|
||||
height: Math.min(materialPreview.width * 0.75, 400)
|
||||
anchors.centerIn: parent
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: "image://materialEditor/preview"
|
||||
cache: false
|
||||
smooth: true
|
||||
|
||||
sourceSize.width: materialPreview.width
|
||||
sourceSize.height: materialPreview.height
|
||||
}
|
||||
}
|
||||
|
||||
HelperWidgets.Section {
|
||||
// Section with hidden header is used so properties are aligned with the other sections' properties
|
||||
hideHeader: true
|
||||
width: parent.width
|
||||
Layout.fillWidth: true
|
||||
collapsible: false
|
||||
|
||||
HelperWidgets.SectionLayout {
|
||||
|
@@ -844,6 +844,7 @@ extend_qtc_plugin(QmlDesigner
|
||||
SOURCES
|
||||
materialeditorcontextobject.cpp materialeditorcontextobject.h
|
||||
materialeditordynamicpropertiesproxymodel.cpp materialeditordynamicpropertiesproxymodel.h
|
||||
materialeditorimageprovider.cpp materialeditorimageprovider.h
|
||||
materialeditorqmlbackend.cpp materialeditorqmlbackend.h
|
||||
materialeditortransaction.cpp materialeditortransaction.h
|
||||
materialeditorview.cpp materialeditorview.h
|
||||
|
@@ -335,7 +335,14 @@ void MaterialBrowserView::selectedNodesChanged(const QList<ModelNode> &selectedN
|
||||
|
||||
void MaterialBrowserView::modelNodePreviewPixmapChanged(const ModelNode &node, const QPixmap &pixmap)
|
||||
{
|
||||
if (isMaterial(node))
|
||||
if (!isMaterial(node))
|
||||
return;
|
||||
|
||||
// There might be multiple requests for different preview pixmap sizes.
|
||||
// Here only the one with the default size is picked.
|
||||
const double ratio = externalDependencies().formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
if (pixmap.width() == dim && pixmap.height() == dim)
|
||||
m_widget->updateMaterialPreview(node, pixmap);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
#include "materialeditorimageprovider.h"
|
||||
|
||||
#include "materialeditorview.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QTimer>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
MaterialEditorImageProvider::MaterialEditorImageProvider(MaterialEditorView *materialEditorView)
|
||||
: QQuickImageProvider(Pixmap)
|
||||
, m_delayTimer(new QTimer(this))
|
||||
{
|
||||
m_delayTimer->setInterval(500);
|
||||
m_delayTimer->setSingleShot(true);
|
||||
m_delayTimer->callOnTimeout([this] {
|
||||
if (m_previewPixmap.size() != m_requestedSize)
|
||||
emit this->requestPreview(m_requestedSize);
|
||||
});
|
||||
|
||||
connect(this,
|
||||
&MaterialEditorImageProvider::requestPreview,
|
||||
materialEditorView,
|
||||
&MaterialEditorView::handlePreviewSizeChanged);
|
||||
}
|
||||
|
||||
void MaterialEditorImageProvider::setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
m_previewPixmap = pixmap;
|
||||
}
|
||||
|
||||
QPixmap MaterialEditorImageProvider::requestPixmap(const QString &id,
|
||||
QSize *size,
|
||||
const QSize &requestedSize)
|
||||
{
|
||||
static QPixmap defaultPreview = QPixmap::fromImage(
|
||||
QImage(":/materialeditor/images/defaultmaterialpreview.png"));
|
||||
|
||||
QPixmap pixmap{150, 150};
|
||||
|
||||
if (id == "preview") {
|
||||
if (!m_previewPixmap.isNull()) {
|
||||
pixmap = m_previewPixmap;
|
||||
setRequestedSize(requestedSize);
|
||||
} else {
|
||||
pixmap = defaultPreview.scaled(requestedSize, Qt::KeepAspectRatio);
|
||||
}
|
||||
} else {
|
||||
qWarning() << __FUNCTION__ << "Unsupported image id:" << id;
|
||||
pixmap.fill(Qt::red);
|
||||
}
|
||||
|
||||
if (size)
|
||||
*size = pixmap.size();
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \internal
|
||||
* \brief Sets the requested size. If the requested size is not the same as the
|
||||
* size of the m_previewPixmap, it will ask \l {MaterialEditorView} to provide
|
||||
* an image with the requested size
|
||||
* The requests are delayed until the requested size is stable.
|
||||
*/
|
||||
void MaterialEditorImageProvider::setRequestedSize(const QSize &requestedSize)
|
||||
{
|
||||
if (!requestedSize.isValid())
|
||||
return;
|
||||
|
||||
m_requestedSize = requestedSize;
|
||||
|
||||
if (m_previewPixmap.size() != requestedSize)
|
||||
m_delayTimer->start();
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QTimer;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class MaterialEditorView;
|
||||
|
||||
class MaterialEditorImageProvider : public QQuickImageProvider
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MaterialEditorImageProvider(MaterialEditorView *materialEditorView);
|
||||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override;
|
||||
|
||||
signals:
|
||||
void requestPreview(QSize);
|
||||
|
||||
private:
|
||||
void setRequestedSize(const QSize &requestedSize);
|
||||
|
||||
QPixmap m_previewPixmap;
|
||||
QSize m_requestedSize;
|
||||
QTimer *m_delayTimer = nullptr; // Delays the preview requests
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -3,9 +3,10 @@
|
||||
|
||||
#include "materialeditorqmlbackend.h"
|
||||
|
||||
#include "propertyeditorvalue.h"
|
||||
#include "materialeditortransaction.h"
|
||||
#include "materialeditorcontextobject.h"
|
||||
#include "materialeditorimageprovider.h"
|
||||
#include "materialeditortransaction.h"
|
||||
#include "propertyeditorvalue.h"
|
||||
#include <qmldesignerconstants.h>
|
||||
#include <qmltimeline.h>
|
||||
|
||||
@@ -22,7 +23,6 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QQuickImageProvider>
|
||||
#include <QQuickItem>
|
||||
#include <QQuickWidget>
|
||||
#include <QVector2D>
|
||||
@@ -39,50 +39,11 @@ static QObject *variantToQObject(const QVariant &value)
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class MaterialEditorImageProvider : public QQuickImageProvider
|
||||
{
|
||||
QPixmap m_previewPixmap;
|
||||
|
||||
public:
|
||||
MaterialEditorImageProvider()
|
||||
: QQuickImageProvider(Pixmap) {}
|
||||
|
||||
void setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
m_previewPixmap = pixmap;
|
||||
}
|
||||
|
||||
QPixmap requestPixmap(const QString &id,
|
||||
QSize *size,
|
||||
[[maybe_unused]] const QSize &requestedSize) override
|
||||
{
|
||||
static QPixmap defaultPreview = QPixmap::fromImage(QImage(":/materialeditor/images/defaultmaterialpreview.png"));
|
||||
|
||||
QPixmap pixmap{150, 150};
|
||||
|
||||
if (id == "preview") {
|
||||
if (!m_previewPixmap.isNull())
|
||||
pixmap = m_previewPixmap;
|
||||
else
|
||||
pixmap = defaultPreview;
|
||||
} else {
|
||||
qWarning() << __FUNCTION__ << "Unsupported image id:" << id;
|
||||
pixmap.fill(Qt::red);
|
||||
}
|
||||
|
||||
|
||||
if (size)
|
||||
*size = pixmap.size();
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
};
|
||||
|
||||
MaterialEditorQmlBackend::MaterialEditorQmlBackend(MaterialEditorView *materialEditor)
|
||||
: m_quickWidget(Utils::makeUniqueObjectPtr<QQuickWidget>())
|
||||
, m_materialEditorTransaction(std::make_unique<MaterialEditorTransaction>(materialEditor))
|
||||
, m_contextObject(std::make_unique<MaterialEditorContextObject>(m_quickWidget.get()))
|
||||
, m_materialEditorImageProvider(new MaterialEditorImageProvider())
|
||||
, m_materialEditorImageProvider(new MaterialEditorImageProvider(materialEditor))
|
||||
{
|
||||
m_quickWidget->setObjectName(Constants::OBJECT_NAME_MATERIAL_EDITOR);
|
||||
m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
@@ -525,6 +525,15 @@ void MaterialEditorView::handlePreviewModelChanged(const QString &modelStr)
|
||||
emitCustomNotification("refresh_material_browser", {});
|
||||
}
|
||||
|
||||
void MaterialEditorView::handlePreviewSizeChanged(const QSizeF &size)
|
||||
{
|
||||
if (m_previewSize == size.toSize())
|
||||
return;
|
||||
|
||||
m_previewSize = size.toSize();
|
||||
requestPreviewRender();
|
||||
}
|
||||
|
||||
void MaterialEditorView::setupQmlBackend()
|
||||
{
|
||||
#ifdef QDS_USE_PROJECTSTORAGE
|
||||
@@ -851,7 +860,9 @@ void MaterialEditorView::propertiesAboutToBeRemoved(const QList<AbstractProperty
|
||||
void MaterialEditorView::requestPreviewRender()
|
||||
{
|
||||
if (model() && model()->nodeInstanceView() && m_selectedMaterial.isValid())
|
||||
model()->nodeInstanceView()->previewImageDataForGenericNode(m_selectedMaterial, {});
|
||||
model()->nodeInstanceView()->previewImageDataForGenericNode(m_selectedMaterial,
|
||||
{},
|
||||
m_previewSize);
|
||||
}
|
||||
|
||||
bool MaterialEditorView::hasWidget() const
|
||||
@@ -937,8 +948,13 @@ void MaterialEditorView::rootNodeTypeChanged(const QString &type, int, int)
|
||||
|
||||
void MaterialEditorView::modelNodePreviewPixmapChanged(const ModelNode &node, const QPixmap &pixmap)
|
||||
{
|
||||
if (node == m_selectedMaterial)
|
||||
m_qmlBackEnd->updateMaterialPreview(pixmap);
|
||||
if (node != m_selectedMaterial)
|
||||
return;
|
||||
|
||||
if (m_previewSize.isValid() && pixmap.size() != m_previewSize)
|
||||
return;
|
||||
|
||||
m_qmlBackEnd->updateMaterialPreview(pixmap);
|
||||
}
|
||||
|
||||
void MaterialEditorView::importsChanged([[maybe_unused]] const Imports &addedImports,
|
||||
|
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QSize>
|
||||
#include <QTimer>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
@@ -83,6 +84,7 @@ public slots:
|
||||
void handleToolBarAction(int action);
|
||||
void handlePreviewEnvChanged(const QString &envAndValue);
|
||||
void handlePreviewModelChanged(const QString &modelStr);
|
||||
void handlePreviewSizeChanged(const QSizeF &size);
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
@@ -124,6 +126,7 @@ private:
|
||||
bool m_hasQuick3DImport = false;
|
||||
bool m_hasMaterialRoot = false;
|
||||
bool m_initializingPreviewData = false;
|
||||
QSize m_previewSize;
|
||||
|
||||
QPointer<QColorDialog> m_colorDialog;
|
||||
QPointer<ItemLibraryInfo> m_itemLibraryInfo;
|
||||
|
@@ -366,7 +366,12 @@ void NavigatorView::enableWidget()
|
||||
|
||||
void NavigatorView::modelNodePreviewPixmapChanged(const ModelNode &node, const QPixmap &pixmap)
|
||||
{
|
||||
m_treeModel->updateToolTipPixmap(node, pixmap);
|
||||
// There might be multiple requests for different preview pixmap sizes.
|
||||
// Here only the one with the default size is picked.
|
||||
const double ratio = externalDependencies().formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
if (pixmap.width() == dim && pixmap.height() == dim)
|
||||
m_treeModel->updateToolTipPixmap(node, pixmap);
|
||||
}
|
||||
|
||||
ModelNode NavigatorView::modelNodeForIndex(const QModelIndex &modelIndex) const
|
||||
|
@@ -134,13 +134,16 @@ public:
|
||||
|
||||
void sendInputEvent(QEvent *e) const;
|
||||
void view3DAction(View3DActionType type, const QVariant &value) override;
|
||||
void requestModelNodePreviewImage(const ModelNode &node, const ModelNode &renderNode) const;
|
||||
void requestModelNodePreviewImage(const ModelNode &node,
|
||||
const ModelNode &renderNode,
|
||||
const QSize &size = {}) const;
|
||||
void edit3DViewResized(const QSize &size) const;
|
||||
|
||||
void handlePuppetToCreatorCommand(const PuppetToCreatorCommand &command) override;
|
||||
|
||||
QVariant previewImageDataForGenericNode(const ModelNode &modelNode,
|
||||
const ModelNode &renderNode) const;
|
||||
const ModelNode &renderNode,
|
||||
const QSize &size = {}) const;
|
||||
QVariant previewImageDataForImageNode(const ModelNode &modelNode) const;
|
||||
|
||||
void setCrashCallback(std::function<void()> crashCallback)
|
||||
|
@@ -1778,9 +1778,6 @@ void NodeInstanceView::handlePuppetToCreatorCommand(const PuppetToCreatorCommand
|
||||
auto node = modelNodeForInternalId(container.instanceId());
|
||||
if (node.isValid()) {
|
||||
const double ratio = m_externalDependencies.formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
if (image.height() != dim || image.width() != dim)
|
||||
image = image.scaled(dim, dim, Qt::KeepAspectRatio);
|
||||
image.setDevicePixelRatio(ratio);
|
||||
updatePreviewImageForNode(node, image);
|
||||
}
|
||||
@@ -1826,13 +1823,15 @@ void NodeInstanceView::view3DAction(View3DActionType type, const QVariant &value
|
||||
}
|
||||
|
||||
void NodeInstanceView::requestModelNodePreviewImage(const ModelNode &node,
|
||||
const ModelNode &renderNode) const
|
||||
const ModelNode &renderNode,
|
||||
const QSize &size) const
|
||||
{
|
||||
if (m_nodeInstanceServer && node.isValid() && hasInstanceForModelNode(node)) {
|
||||
auto instance = instanceForModelNode(node);
|
||||
if (instance.isValid()) {
|
||||
qint32 renderItemId = -1;
|
||||
QString componentPath;
|
||||
QSize imageSize;
|
||||
if (renderNode.isValid()) {
|
||||
auto renderInstance = instanceForModelNode(renderNode);
|
||||
if (renderInstance.isValid())
|
||||
@@ -1842,11 +1841,17 @@ void NodeInstanceView::requestModelNodePreviewImage(const ModelNode &node,
|
||||
} else if (node.isComponent()) {
|
||||
componentPath = ModelUtils::componentFilePath(node);
|
||||
}
|
||||
const double ratio = m_externalDependencies.formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
m_nodeInstanceServer->requestModelNodePreviewImage(
|
||||
RequestModelNodePreviewImageCommand(instance.instanceId(), QSize(dim, dim),
|
||||
componentPath, renderItemId));
|
||||
|
||||
if (size.isValid()) {
|
||||
imageSize = size;
|
||||
} else {
|
||||
const double ratio = m_externalDependencies.formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
imageSize = {dim, dim};
|
||||
}
|
||||
|
||||
m_nodeInstanceServer->requestModelNodePreviewImage(RequestModelNodePreviewImageCommand(
|
||||
instance.instanceId(), imageSize, componentPath, renderItemId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1991,7 +1996,8 @@ void NodeInstanceView::endNanotrace()
|
||||
}
|
||||
|
||||
QVariant NodeInstanceView::previewImageDataForGenericNode(const ModelNode &modelNode,
|
||||
const ModelNode &renderNode) const
|
||||
const ModelNode &renderNode,
|
||||
const QSize &size) const
|
||||
{
|
||||
if (!modelNode.isValid())
|
||||
return {};
|
||||
@@ -2006,9 +2012,15 @@ QVariant NodeInstanceView::previewImageDataForGenericNode(const ModelNode &model
|
||||
} else {
|
||||
imageData.type = QString::fromLatin1(createQualifiedTypeName(modelNode));
|
||||
imageData.id = id;
|
||||
m_imageDataMap.insert(id, imageData);
|
||||
|
||||
// There might be multiple requests for different preview pixmap sizes.
|
||||
// Here only the one with the default size is stored.
|
||||
const double ratio = externalDependencies().formEditorDevicePixelRatio();
|
||||
const int dim = Constants::MODELNODE_PREVIEW_IMAGE_DIMENSIONS * ratio;
|
||||
if (size.width() == dim && size.height() == dim)
|
||||
m_imageDataMap.insert(id, imageData);
|
||||
}
|
||||
requestModelNodePreviewImage(modelNode, renderNode);
|
||||
requestModelNodePreviewImage(modelNode, renderNode, size);
|
||||
|
||||
return modelNodePreviewImageDataToVariant(imageData);
|
||||
}
|
||||
@@ -2028,7 +2040,6 @@ void NodeInstanceView::updateWatcher(const QString &path)
|
||||
QStringList oldDirs;
|
||||
QStringList newFiles;
|
||||
QStringList newDirs;
|
||||
QStringList qsbFiles;
|
||||
|
||||
const QString projPath = m_externalDependencies.currentProjectDirPath();
|
||||
|
||||
|
Reference in New Issue
Block a user