diff --git a/qml/PropertiesPanel.qml b/qml/PropertiesPanel.qml index 9a8c594c..9ba04ccc 100644 --- a/qml/PropertiesPanel.qml +++ b/qml/PropertiesPanel.qml @@ -4363,7 +4363,45 @@ Rectangle { font.bold: true anchors.verticalCenter: parent.verticalCenter } - Item { width: parent.width - 260; height: 1 } + Item { width: parent.width - 320; height: 1 } + // Add from current edit — captures the user's current + // edit-mode geometry minus the bind-pose baseline as + // a new morph target. Disabled (greyed out, forbidden + // cursor) when outside edit mode because + // EditableSubMesh::originalPositions is only + // populated by EditModeController and the C++ method + // would return false anyway. + Rectangle { + id: addBtn + property bool canAddFromEdit: EditModeController.editModeActive + width: 56; height: 20; radius: 3 + opacity: canAddFromEdit ? 1.0 : 0.45 + color: addMa.containsMouse && canAddFromEdit + ? Qt.lighter(PropertiesPanelController.headerColor, 1.3) + : PropertiesPanelController.controlBgColor + border.color: PropertiesPanelController.borderColor + anchors.verticalCenter: parent.verticalCenter + Text { + anchors.centerIn: parent + text: "+ Add…" + color: PropertiesPanelController.textColor + font.pixelSize: 9 + } + MouseArea { + id: addMa + anchors.fill: parent + hoverEnabled: true + enabled: addBtn.canAddFromEdit + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor + onClicked: { + addNameField.text = "" + addError.text = "" + addNamePopup.open() + } + ToolTip.visible: containsMouse && !enabled + ToolTip.text: "Enter Edit Mode (Tab) to add morph targets from current edit." + } + } // Reset all: walks every target and sets weight to 0. Rectangle { width: 60; height: 20; radius: 3 @@ -4391,6 +4429,100 @@ Rectangle { } } + // Inline name-entry popup for "Add from edit…". Kept + // simple (no styled component) so a misbehaving custom + // dialog can't break the rest of the panel — Popup is + // a built-in Qt Quick Controls primitive with no + // singleton dependencies. + Popup { + id: addNamePopup + modal: true + focus: true + width: 240 + contentItem: Column { + spacing: 6 + Text { + text: "New morph target name:" + color: PropertiesPanelController.textColor + font.pixelSize: 11 + } + TextField { + id: addNameField + width: 220 + font.pixelSize: 11 + onAccepted: addConfirmMa.confirm() + onTextChanged: addError.text = "" + Component.onCompleted: forceActiveFocus() + } + // Inline error: shown when the C++ side rejects + // the request (duplicate name, no vertex moved, + // not in edit mode, …). We deliberately keep the + // popup open so the user can fix the input + // without retyping. + Text { + id: addError + text: "" + visible: text.length > 0 + color: "#d65d5d" + font.pixelSize: 10 + width: 220 + wrapMode: Text.Wrap + } + Row { + spacing: 6 + Rectangle { + width: 60; height: 20; radius: 3 + color: addConfirmMa.containsMouse + ? Qt.lighter(PropertiesPanelController.headerColor, 1.3) + : PropertiesPanelController.controlBgColor + border.color: PropertiesPanelController.borderColor + Text { anchors.centerIn: parent; text: "Save"; color: PropertiesPanelController.textColor; font.pixelSize: 10 } + MouseArea { + id: addConfirmMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + function confirm() { + var n = addNameField.text.trim() + if (n.length === 0) { + addError.text = "Name cannot be empty." + return + } + if (!EditModeController.editModeActive) { + addError.text = "Enter Edit Mode (Tab) before saving." + return + } + var ok = MorphAnimationManager.addMorphTargetFromCurrentEdit(n) + if (ok) { + addNamePopup.close() + } else { + // C++ rejected — likely name collision or + // no vertex moved vs the bind baseline. + addError.text = "Couldn't save: name already in use, or no vertex was edited." + } + } + onClicked: confirm() + } + } + Rectangle { + width: 60; height: 20; radius: 3 + color: addCancelMa.containsMouse + ? Qt.lighter(PropertiesPanelController.headerColor, 1.3) + : PropertiesPanelController.controlBgColor + border.color: PropertiesPanelController.borderColor + Text { anchors.centerIn: parent; text: "Cancel"; color: PropertiesPanelController.textColor; font.pixelSize: 10 } + MouseArea { + id: addCancelMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: addNamePopup.close() + } + } + } + } + } + // Filter / search — characters often have 50+ blend // shapes, scanning a flat list is hopeless without // a typeahead box. @@ -4413,18 +4545,63 @@ Rectangle { || modelData.toLowerCase().indexOf(morphCol.filter.toLowerCase()) >= 0 height: visible ? 22 : 0 + // Name — double-click to rename in place, + // matching the per-animation rename UX above. Text { + id: morphNameText + visible: !morphNameEdit.visible text: modelData color: PropertiesPanelController.textColor font.pixelSize: 10 width: 120 elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent + onDoubleClicked: { + morphNameEdit.text = modelData + morphNameEdit.visible = true + morphNameEdit.forceActiveFocus() + morphNameEdit.selectAll() + } + } + } + TextInput { + id: morphNameEdit + visible: false + width: 120 + color: PropertiesPanelController.textColor + font.pixelSize: 10 + anchors.verticalCenter: parent.verticalCenter + selectByMouse: true + // Set by `Keys.onEscapePressed`; checked in + // `onEditingFinished` so that hiding the + // input on Escape (which causes focus loss + // and fires `editingFinished`) doesn't + // accidentally commit the rename. + property bool cancelled: false + Rectangle { + anchors.fill: parent + anchors.margins: -2 + z: -1 + color: PropertiesPanelController.inputColor + border.color: PropertiesPanelController.highlightColor + border.width: 1 + radius: 2 + } + onEditingFinished: { + if (cancelled) { cancelled = false; visible = false; return } + var trimmed = text.trim() + if (trimmed.length > 0 && trimmed !== modelData) + MorphAnimationManager.renameMorphTarget(modelData, trimmed) + visible = false + } + Keys.onEscapePressed: { cancelled = true; visible = false } } Slider { id: weightSlider from: 0; to: 1; stepSize: 0.01 - width: parent.width - 200 + width: parent.width - 222 // Bind to `weightTick` so changes that // bypass user drag (Reset all, MCP, future // dope-sheet scrubs) refresh the readout. @@ -4440,6 +4617,30 @@ Rectangle { width: 36 anchors.verticalCenter: parent.verticalCenter } + // Delete (×) — drops the pose + animation + // through DeleteMorphTargetCommand so Ctrl+Z + // restores it. + Rectangle { + width: 18; height: 18; radius: 3 + anchors.verticalCenter: parent.verticalCenter + color: morphDelMa.containsMouse + ? Qt.lighter(PropertiesPanelController.headerColor, 1.3) + : "transparent" + Text { + anchors.centerIn: parent + text: "×" + color: PropertiesPanelController.textColor + font.pixelSize: 12 + font.bold: true + } + MouseArea { + id: morphDelMa + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: MorphAnimationManager.deleteMorphTarget(modelData) + } + } } } }