Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions qml/PropertiesPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4415,6 +4415,9 @@ Rectangle {
property bool hasMask: TexturePaintController.hasSelectionMask
property int maskCount: TexturePaintController.selectedPixelCount
property real smartTolerance: TexturePaintController.smartSelectTolerance
property int layerCount: TexturePaintController.layerCount
property int activeLayerIndex: TexturePaintController.activeLayerIndex
property var paintLayers: TexturePaintController.paintLayers
// Live hover position in UV space, fed by hoveredUVChanged.
property real hoverU: -1
property real hoverV: -1
Expand Down Expand Up @@ -4451,6 +4454,11 @@ Rectangle {
texPaintCol.maskCount = TexturePaintController.selectedPixelCount
texPaintCol.smartTolerance = TexturePaintController.smartSelectTolerance
}
function onLayersChanged() {
texPaintCol.layerCount = TexturePaintController.layerCount
texPaintCol.activeLayerIndex = TexturePaintController.activeLayerIndex
texPaintCol.paintLayers = TexturePaintController.paintLayers
}
}

Text {
Expand Down Expand Up @@ -4699,6 +4707,270 @@ Rectangle {
wrapMode: Text.Wrap
}

// ---- Layers (Paint v2 Slice C #546) ----
Column {
id: layersCol
spacing: 6
width: parent.width - 16
visible: texPaintCol.hasSession && texPaintCol.paintTarget === 0

property var activeLayer: {
const layers = texPaintCol.paintLayers || []
return (layers.length > texPaintCol.activeLayerIndex)
? layers[texPaintCol.activeLayerIndex] : null
}

Text {
text: "Layers"
color: PropertiesPanelController.textColor
font.pixelSize: 11
font.bold: true
}

// Toolbar — same action-string pattern as the smart-select row.
Row {
spacing: 3
width: parent.width
Repeater {
model: [
{ label: "+", action: "add", hint: "Add layer" },
{ label: "Dup", action: "dup", hint: "Duplicate" },
{ label: "\u2191", action: "up", hint: "Move up" },
{ label: "\u2193", action: "down", hint: "Move down" },
{ label: "Mrg", action: "merge", hint: "Merge down" },
{ label: "Flat", action: "flatten", hint: "Flatten all" },
{ label: "\u2715", action: "delete", hint: "Delete layer" }
]
Rectangle {
width: 34; height: 22; radius: 3
property bool btnEnabled: modelData.action !== "delete"
|| texPaintCol.layerCount > 1
opacity: btnEnabled ? 1.0 : 0.35
color: btnEnabled && layerBtnMa.containsMouse
? Qt.lighter(PropertiesPanelController.panelColor, 1.4)
: PropertiesPanelController.headerColor
border.color: PropertiesPanelController.borderColor
Text {
anchors.centerIn: parent
text: modelData.label
color: PropertiesPanelController.textColor
font.pixelSize: 9
}
MouseArea {
id: layerBtnMa
anchors.fill: parent
hoverEnabled: btnEnabled
enabled: btnEnabled
cursorShape: btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
ToolTip.text: !btnEnabled && modelData.action === "delete"
? "Cannot delete the last layer"
: modelData.hint
ToolTip.visible: containsMouse
ToolTip.delay: 400
onClicked: {
const idx = texPaintCol.activeLayerIndex
switch (modelData.action) {
case "add": TexturePaintController.addPaintLayer(""); break
case "dup": TexturePaintController.duplicatePaintLayer(idx); break
case "up": TexturePaintController.movePaintLayerUp(idx); break
case "down": TexturePaintController.movePaintLayerDown(idx); break
case "merge": TexturePaintController.mergePaintLayerDown(idx); break
case "flatten": TexturePaintController.flattenPaintLayers(); break
case "delete": TexturePaintController.deletePaintLayer(idx); break
}
}
}
}
}
}

ListView {
id: layerList
width: parent.width
height: Math.min(130, Math.max(36, count * 36))
clip: true
spacing: 2
model: texPaintCol.paintLayers

delegate: Rectangle {
width: layerList.width
height: 34
radius: 3
color: modelData.active
? Qt.darker(PropertiesPanelController.highlightColor, 1.2)
: (layerRowMa.containsMouse ? Qt.lighter(PropertiesPanelController.panelColor, 1.3)
: PropertiesPanelController.headerColor)
border.color: modelData.active ? PropertiesPanelController.highlightColor
: PropertiesPanelController.borderColor

Row {
anchors.fill: parent
anchors.margins: 3
spacing: 4

Image {
width: 28; height: 28
source: modelData.thumbnailUrl
fillMode: Image.PreserveAspectFit
smooth: false
cache: false
}

Text {
width: Math.max(40, layerList.width - 130)
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: PropertiesPanelController.textColor
font.pixelSize: 10
elide: Text.ElideRight
}

// Visible toggle (eye)
Rectangle {
width: 18; height: 18; radius: 3
anchors.verticalCenter: parent.verticalCenter
color: modelData.visible
? PropertiesPanelController.highlightColor
: PropertiesPanelController.controlBgColor
border.color: PropertiesPanelController.borderColor; border.width: 1
Text {
anchors.centerIn: parent
text: modelData.visible ? "\u2713" : ""
color: "white"; font.pixelSize: 9
}
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: TexturePaintController.setPaintLayerVisible(
modelData.index, !modelData.visible)
}
}

// Solo toggle
Rectangle {
width: 18; height: 18; radius: 3
anchors.verticalCenter: parent.verticalCenter
color: modelData.solo ? "#806622" : PropertiesPanelController.controlBgColor
border.color: PropertiesPanelController.borderColor; border.width: 1
Text {
anchors.centerIn: parent
text: "S"
color: modelData.solo ? "#ffcc00" : PropertiesPanelController.textColor
font.pixelSize: 8; font.bold: true
}
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: TexturePaintController.setPaintLayerSolo(
modelData.index, !modelData.solo)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Lock toggle
Rectangle {
width: 18; height: 18; radius: 3
anchors.verticalCenter: parent.verticalCenter
color: modelData.locked
? "#804040" : PropertiesPanelController.controlBgColor
border.color: PropertiesPanelController.borderColor; border.width: 1
Text {
anchors.centerIn: parent
text: modelData.locked ? "L" : ""
color: modelData.locked ? "#ffcccc" : PropertiesPanelController.textColor
font.pixelSize: 8; font.bold: true
}
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: TexturePaintController.setPaintLayerLocked(
modelData.index, !modelData.locked)
}
}
}

MouseArea {
id: layerRowMa
anchors.fill: parent
z: -1
hoverEnabled: true
onClicked: TexturePaintController.activeLayerIndex = modelData.index
}
Comment on lines +4730 to +4894

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Make the layer toolbar and list keyboard accessible.

The toolbar buttons, layer selection, visibility, and solo controls are mouse-only. Use ToolButton/CheckBox/ItemDelegate, or add tab focus, accessible roles/names, focus indicators, and Space/Enter handlers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qml/PropertiesPanel.qml` around lines 4727 - 4871, Make the layer toolbar and
layerList controls keyboard accessible: replace or augment the toolbar
rectangles, layer-selection MouseArea, and visibility/solo controls with
focusable controls that expose accessible roles and descriptive names, show a
visible focus indicator, and invoke the existing actions on Space or Enter.
Preserve the current enabled-state behavior and layer action symbols, including
modelData.action, modelData.index, and the existing TexturePaintController
calls.

}
}

Row {
spacing: 6
width: parent.width
Text {
text: "Opacity"
width: 52
color: PropertiesPanelController.textColor
font.pixelSize: 10
anchors.verticalCenter: parent.verticalCenter
}
Slider {
id: layerOpacitySlider
width: 120
from: 0; to: 1; stepSize: 0.01
property bool updating: false
value: layersCol.activeLayer ? layersCol.activeLayer.opacity : 1
onPressedChanged: {
if (pressed)
TexturePaintController.beginPaintLayerOpacityDrag()
else
TexturePaintController.endPaintLayerOpacityDrag()
}
onMoved: {
if (!layersCol.activeLayer) return
TexturePaintController.setPaintLayerOpacity(
texPaintCol.activeLayerIndex, value)
}
Connections {
target: TexturePaintController
function onLayersChanged() {
if (!layersCol.activeLayer) return
layerOpacitySlider.updating = true
layerOpacitySlider.value = layersCol.activeLayer.opacity
layerOpacitySlider.updating = false
}
}
}
Text {
text: layersCol.activeLayer
? Math.round(layersCol.activeLayer.opacity * 100) + "%" : "100%"
color: PropertiesPanelController.textColor
font.pixelSize: 10
anchors.verticalCenter: parent.verticalCenter
}
}

Row {
spacing: 6
width: parent.width
Text {
text: "Blend"
width: 52
color: PropertiesPanelController.textColor
font.pixelSize: 10
anchors.verticalCenter: parent.verticalCenter
}
ThemedComboBox {
id: layerBlendCombo
width: Math.max(120, layersCol.width - 58)
model: TexturePaintController.blendModeNames
currentIndex: layersCol.activeLayer ? layersCol.activeLayer.blendMode : 0
onActivated: function(index) {
TexturePaintController.setPaintLayerBlendMode(
texPaintCol.activeLayerIndex, index)
}
Connections {
target: TexturePaintController
function onLayersChanged() {
if (!layersCol.activeLayer) return
layerBlendCombo.currentIndex = layersCol.activeLayer.blendMode
}
}
}
}
}

// ---- 2D preview / paint surface ----
// Live image of the paint buffer; clicking and dragging
// paints into the texture in UV space. Crosshair indicator
Expand Down
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ MultiViewTextureBaker.cpp
TextureChannelPacker.cpp
TextureAtlasPacker.cpp
PaintBufferImageProvider.cpp
PaintLayerBlend.cpp
PaintLayerStack.cpp
PaintSelectionMask.cpp
GradientRamp.cpp
BrushEngine.cpp
Expand Down Expand Up @@ -367,6 +369,8 @@ MultiViewTextureBaker.h
TextureChannelPacker.h
TextureAtlasPacker.h
PaintBufferImageProvider.h
PaintLayerBlend.h
PaintLayerStack.h
PaintSelectionMask.h
GradientRamp.h
BrushEngine.h
Expand Down
10 changes: 10 additions & 0 deletions src/MeshImporterExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
#include "PS1/PS1TIM.h"
#include "EditableMesh.h"
#include "EditModeController.h"
#include "TexturePaintController.h"
#include <OgreMaterialManager.h>
#include <OgreRTShaderSystem.h>
#include <OgreDataStream.h>
Expand Down Expand Up @@ -3379,6 +3380,13 @@
return QString();
}

if (Manager::getSingleton()->getSceneMgr()->hasEntity(_sn->getName())) {
if (auto* tpc = TexturePaintController::instance()) {

Check warning on line 3384 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this variable a pointer-to-const. The current type of "tpc" is "class TexturePaintController *".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ-vxXQEARI_fVuNLyO_&open=AZ-vxXQEARI_fVuNLyO_&pullRequest=935
if (!tpc->confirmFlattenLayersForExport(parent))
return QString();
}
}

QString filter = "Ogre Mesh (*.mesh)";
QString fileName = QFileDialog::getSaveFileName(parent, QObject::tr("Export Mesh"),
_sn->getName().data(),
Expand Down Expand Up @@ -3410,6 +3418,8 @@
// Vertex paint defers GPU upload; export reads Ogre buffers — sync first.
EditModeController::instance()->flushPendingVertexPaintForEntity(
const_cast<Ogre::Entity*>(e));
if (auto* tpc = TexturePaintController::instance())
tpc->flushPaintTextureForExport(const_cast<Ogre::Entity*>(e));

Check failure on line 3422 in src/MeshImporterExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

const_cast removing const qualification from the type of a pointer may lead to an undefined behaviour.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ-vxXQEARI_fVuNLyPA&open=AZ-vxXQEARI_fVuNLyPA&pullRequest=935

if(_format=="Ogre XML (*.mesh.xml)")
{
Expand Down
16 changes: 13 additions & 3 deletions src/PaintBufferImageProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ PaintBufferImageProvider::PaintBufferImageProvider()
QImage PaintBufferImageProvider::requestImage(
const QString& id, QSize* size, const QSize& requestedSize)
{
Q_UNUSED(id); // We only have one "image" — the live buffer.
Q_UNUSED(requestedSize); // QML's sourceSize hint is honoured by Image{}.
Q_UNUSED(requestedSize);

auto* tpc = TexturePaintController::instance();
if (!tpc) return {};
QImage img = tpc->snapshotBufferImage();

QImage img;
if (id.startsWith(QStringLiteral("layer/"))) {
const QString idxStr = id.mid(6);
bool ok = false;
const int layerIndex = idxStr.toInt(&ok);
if (ok)
img = tpc->snapshotLayerImage(layerIndex);
} else {
img = tpc->snapshotBufferImage();
}

if (size) *size = img.size();
return img;
}
Loading
Loading