diff --git a/faststack/qml/HistogramModeToggle.qml b/faststack/qml/HistogramModeToggle.qml new file mode 100644 index 0000000..49228e5 --- /dev/null +++ b/faststack/qml/HistogramModeToggle.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Layouts 1.15 + +RowLayout { + id: root + + property bool overlaidMode: true + signal modeRequested(bool overlaid) + + Item { Layout.fillWidth: true } + + Row { + spacing: 0 + + Rectangle { + width: 70; height: 20 + radius: 3 + color: root.overlaidMode ? "#2c2c2c" : "transparent" + border.color: "#3a3a3a"; border.width: 1 + + Text { + anchors.centerIn: parent + text: "Overlaid" + font.pixelSize: 10 + color: root.overlaidMode ? "#e8e6e3" : "#6b6764" + } + + MouseArea { + anchors.fill: parent + onClicked: root.modeRequested(true) + } + } + + Rectangle { + width: 70; height: 20 + radius: 3 + color: !root.overlaidMode ? "#2c2c2c" : "transparent" + border.color: "#3a3a3a"; border.width: 1 + + Text { + anchors.centerIn: parent + text: "Channels" + font.pixelSize: 10 + color: !root.overlaidMode ? "#e8e6e3" : "#6b6764" + } + + MouseArea { + anchors.fill: parent + onClicked: root.modeRequested(false) + } + } + } +} diff --git a/faststack/qml/HistogramWindow.qml b/faststack/qml/HistogramWindow.qml index 2a7d6e3..17a0a6e 100644 --- a/faststack/qml/HistogramWindow.qml +++ b/faststack/qml/HistogramWindow.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Window import QtQuick.Layouts 1.15 +import QtCore Window { id: histogramWindow @@ -11,6 +12,12 @@ Window { minimumHeight: 50 property var uiStateRef: typeof uiState !== "undefined" ? uiState : null property var controllerRef: typeof controller !== "undefined" ? controller : null + Settings { + id: histSettings + category: "histogram" + property bool overlaidMode: true + } + visible: histogramWindow.uiStateRef ? histogramWindow.uiStateRef.isHistogramVisible : false FocusScope { @@ -51,57 +58,100 @@ Window { property color primaryTextColor: "#222222" property color gridLineColor: "#dcdcdc" property color dangerColor: Qt.rgba(1, 0, 0, 0.25) + property var histogramDataRef: histogramWindow.uiStateRef ? histogramWindow.uiStateRef.histogramData : null + property var rHistogramData: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["r"] || []) : [] + property var gHistogramData: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["g"] || []) : [] + property var bHistogramData: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["b"] || []) : [] + property int rClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["r_clip"] || 0) : 0 + property int gClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["g_clip"] || 0) : 0 + property int bClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["b_clip"] || 0) : 0 + property int rPreClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["r_preclip"] || 0) : 0 + property int gPreClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["g_preclip"] || 0) : 0 + property int bPreClipCount: histogramWindow.histogramDataRef ? (histogramWindow.histogramDataRef["b_preclip"] || 0) : 0 color: windowBackgroundColor - RowLayout { + ColumnLayout { anchors.fill: parent anchors.margins: histogramWindow.width > 200 ? 15 : 2 - spacing: histogramWindow.width > 200 ? 15 : 2 + spacing: histogramWindow.width > 200 ? 8 : 2 - SingleChannelHistogram { - Layout.fillWidth: true - Layout.fillHeight: true - - channelName: "Red" - channelColor: "#e15050" - gridLineColor: histogramWindow.gridLineColor - dangerColor: histogramWindow.dangerColor - textColor: histogramWindow.primaryTextColor - - histogramData: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["r"] || []) : [] - clipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["r_clip"] || 0) : 0 - preClipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["r_preclip"] || 0) : 0 - } - - SingleChannelHistogram { + HistogramModeToggle { Layout.fillWidth: true - Layout.fillHeight: true - - channelName: "Green" - channelColor: "#50e150" - gridLineColor: histogramWindow.gridLineColor - dangerColor: histogramWindow.dangerColor - textColor: histogramWindow.primaryTextColor - - histogramData: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["g"] || []) : [] - clipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["g_clip"] || 0) : 0 - preClipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["g_preclip"] || 0) : 0 + overlaidMode: histSettings.overlaidMode + onModeRequested: (overlaid) => histSettings.overlaidMode = overlaid } - SingleChannelHistogram { + // Histogram display (overlaid or 3-channel) + Item { Layout.fillWidth: true Layout.fillHeight: true - - channelName: "Blue" - channelColor: "#5050e1" - gridLineColor: histogramWindow.gridLineColor - dangerColor: histogramWindow.dangerColor - textColor: histogramWindow.primaryTextColor - - histogramData: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["b"] || []) : [] - clipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["b_clip"] || 0) : 0 - preClipCount: histogramWindow.uiStateRef && histogramWindow.uiStateRef.histogramData ? (histogramWindow.uiStateRef.histogramData["b_preclip"] || 0) : 0 + + OverlaidHistogram { + anchors.fill: parent + visible: histSettings.overlaidMode + rData: histogramWindow.rHistogramData + gData: histogramWindow.gHistogramData + bData: histogramWindow.bHistogramData + rClip: histogramWindow.rClipCount + gClip: histogramWindow.gClipCount + bClip: histogramWindow.bClipCount + rPreClip: histogramWindow.rPreClipCount + gPreClip: histogramWindow.gPreClipCount + bPreClip: histogramWindow.bPreClipCount + gridLineColor: histogramWindow.gridLineColor + } + + RowLayout { + anchors.fill: parent + visible: !histSettings.overlaidMode + spacing: histogramWindow.width > 200 ? 15 : 2 + + SingleChannelHistogram { + Layout.fillWidth: true + Layout.fillHeight: true + + channelName: "Red" + channelColor: "#e15050" + gridLineColor: histogramWindow.gridLineColor + dangerColor: histogramWindow.dangerColor + textColor: histogramWindow.primaryTextColor + + histogramData: histogramWindow.rHistogramData + clipCount: histogramWindow.rClipCount + preClipCount: histogramWindow.rPreClipCount + } + + SingleChannelHistogram { + Layout.fillWidth: true + Layout.fillHeight: true + + channelName: "Green" + channelColor: "#50e150" + gridLineColor: histogramWindow.gridLineColor + dangerColor: histogramWindow.dangerColor + textColor: histogramWindow.primaryTextColor + + histogramData: histogramWindow.gHistogramData + clipCount: histogramWindow.gClipCount + preClipCount: histogramWindow.gPreClipCount + } + + SingleChannelHistogram { + Layout.fillWidth: true + Layout.fillHeight: true + + channelName: "Blue" + channelColor: "#5050e1" + gridLineColor: histogramWindow.gridLineColor + dangerColor: histogramWindow.dangerColor + textColor: histogramWindow.primaryTextColor + + histogramData: histogramWindow.bHistogramData + clipCount: histogramWindow.bClipCount + preClipCount: histogramWindow.bPreClipCount + } + } } } } diff --git a/faststack/qml/ImageEditorDialog.qml b/faststack/qml/ImageEditorDialog.qml index 005f617..e783034 100644 --- a/faststack/qml/ImageEditorDialog.qml +++ b/faststack/qml/ImageEditorDialog.qml @@ -5,6 +5,7 @@ import QtQuick.Controls 2.15 import QtQuick.Controls.Material 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 +import QtCore Window { id: imageEditorDialog @@ -15,9 +16,17 @@ Window { title: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.editorFilename ? "Image Editor - " + imageEditorDialog.uiStateRef.editorFilename + " (" + imageEditorDialog.uiStateRef.editorBitDepth + "-bit)" : "Image Editor" visible: imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.isEditorOpen : false flags: Qt.Window | Qt.WindowTitleHint | Qt.WindowCloseButtonHint + Settings { + id: histSettings + category: "histogram" + property bool overlaidMode: true + } + property int updatePulse: 0 property color backgroundColor: "#1e1e1e" // Default dark background property color textColor: "white" // Default text color + property bool sourceExpanded: true + readonly property int secondaryButtonHeight: 30 // Modern Color Palette readonly property color accentColor: "#6366f1" // Modern Indigo @@ -105,13 +114,28 @@ Window { // Component for Section Header Component { id: sectionHeader - Label { - font.bold: true - font.pixelSize: 15 - font.letterSpacing: 1.0 - color: imageEditorDialog.accentColorHover + ColumnLayout { + property alias text: sectionHeaderText.text + + Layout.fillWidth: true Layout.topMargin: 5 Layout.bottomMargin: 10 + spacing: 6 + + Text { + id: sectionHeaderText + Layout.fillWidth: true + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + color: "#9a9795" + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: "#2e2e2e" + } } } @@ -137,7 +161,7 @@ Window { Loader { sourceComponent: sectionHeader Layout.topMargin: 0 // Remove top margin for the very first item - onLoaded: item.text = "☀ Light" + onLoaded: item.text = "LIGHT" } ListModel { id: lightModel @@ -156,7 +180,7 @@ Window { // --- Detail Group --- Loader { sourceComponent: sectionHeader - onLoaded: item.text = "🔍 Detail" + onLoaded: item.text = "DETAIL" } ListModel { id: detailModel @@ -166,59 +190,65 @@ Window { } Repeater { model: detailModel; delegate: editSlider } - // --- Histogram Group --- - RowLayout { + HistogramModeToggle { + Layout.fillWidth: true + Layout.topMargin: 8 + overlaidMode: histSettings.overlaidMode + onModeRequested: (overlaid) => histSettings.overlaidMode = overlaid + } + + // Histogram display (overlaid or 3-channel) + Item { Layout.fillWidth: true Layout.preferredHeight: 140 - Layout.topMargin: 5 - spacing: 5 - - SingleChannelHistogram { - Layout.fillWidth: true - Layout.fillHeight: true - - channelName: "Red" - channelColor: "#e15050" - gridLineColor: imageEditorDialog.controlBorder - dangerColor: "#40ff0000" - textColor: imageEditorDialog.textColor - minimal: false - - histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r"] || []) : [] - clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_clip"] || 0) : 0 - preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_preclip"] || 0) : 0 - } - - SingleChannelHistogram { - Layout.fillWidth: true - Layout.fillHeight: true - - channelName: "Green" - channelColor: "#50e150" + + OverlaidHistogram { + anchors.fill: parent + visible: histSettings.overlaidMode + rData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r"] || []) : [] + gData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g"] || []) : [] + bData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b"] || []) : [] + rClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_clip"] || 0) : 0 + gClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_clip"] || 0) : 0 + bClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_clip"] || 0) : 0 + rPreClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_preclip"] || 0) : 0 + gPreClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_preclip"] || 0) : 0 + bPreClip: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_preclip"] || 0) : 0 gridLineColor: imageEditorDialog.controlBorder - dangerColor: "#40ff0000" - textColor: imageEditorDialog.textColor - minimal: false - - histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g"] || []) : [] - clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_clip"] || 0) : 0 - preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_preclip"] || 0) : 0 } - SingleChannelHistogram { - Layout.fillWidth: true - Layout.fillHeight: true - - channelName: "Blue" - channelColor: "#5050e1" - gridLineColor: imageEditorDialog.controlBorder - dangerColor: "#40ff0000" - textColor: imageEditorDialog.textColor - minimal: false - - histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b"] || []) : [] - clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_clip"] || 0) : 0 - preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_preclip"] || 0) : 0 + RowLayout { + anchors.fill: parent + visible: !histSettings.overlaidMode + spacing: 5 + + SingleChannelHistogram { + Layout.fillWidth: true; Layout.fillHeight: true + channelName: "Red"; channelColor: "#e15050" + gridLineColor: imageEditorDialog.controlBorder + dangerColor: "#40ff0000"; textColor: imageEditorDialog.textColor; minimal: false + histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r"] || []) : [] + clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_clip"] || 0) : 0 + preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["r_preclip"] || 0) : 0 + } + SingleChannelHistogram { + Layout.fillWidth: true; Layout.fillHeight: true + channelName: "Green"; channelColor: "#50e150" + gridLineColor: imageEditorDialog.controlBorder + dangerColor: "#40ff0000"; textColor: imageEditorDialog.textColor; minimal: false + histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g"] || []) : [] + clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_clip"] || 0) : 0 + preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["g_preclip"] || 0) : 0 + } + SingleChannelHistogram { + Layout.fillWidth: true; Layout.fillHeight: true + channelName: "Blue"; channelColor: "#5050e1" + gridLineColor: imageEditorDialog.controlBorder + dangerColor: "#40ff0000"; textColor: imageEditorDialog.textColor; minimal: false + histogramData: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b"] || []) : [] + clipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_clip"] || 0) : 0 + preClipCount: imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.histogramData ? (imageEditorDialog.uiStateRef.histogramData["b_preclip"] || 0) : 0 + } } } @@ -254,24 +284,88 @@ Window { spacing: 15 // --- Source Group --- - Loader { - sourceComponent: sectionHeader + ColumnLayout { + Layout.fillWidth: true Layout.topMargin: 0 // Remove top margin for the very first item - onLoaded: item.text = "📸 Source" visible: imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.hasRaw : false + spacing: 6 + + RowLayout { + Layout.fillWidth: true + spacing: 6 + + Text { + text: imageEditorDialog.sourceExpanded ? "▾" : "▸" + font.pixelSize: 10 + color: "#9a9795" + Layout.alignment: Qt.AlignVCenter + } + + Text { + text: "SOURCE" + font.pixelSize: 10 + font.weight: Font.DemiBold + font.letterSpacing: 1.2 + color: "#9a9795" + Layout.alignment: Qt.AlignVCenter + } + + Rectangle { + Layout.alignment: Qt.AlignVCenter + implicitWidth: sourceBadgeText.implicitWidth + 10 + implicitHeight: sourceBadgeText.implicitHeight + 4 + color: "#2a2010" + border.color: "#3a3010" + border.width: 1 + radius: 3 + + Text { + id: sourceBadgeText + anchors.centerIn: parent + text: "experimental" + font.pixelSize: 9 + font.family: "IBM Plex Mono" + color: "#7a6a3a" + } + } + + Item { Layout.fillWidth: true } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: "#2e2e2e" + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: imageEditorDialog.sourceExpanded = !imageEditorDialog.sourceExpanded + } } - Button { - text: (imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.isRawActive) ? "RAW Loaded" : "Load RAW" + + ColumnLayout { Layout.fillWidth: true - visible: imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.hasRaw : false - enabled: imageEditorDialog.uiStateRef ? !imageEditorDialog.uiStateRef.isRawActive : false - onClicked: { - if (imageEditorDialog.uiStateRef) imageEditorDialog.uiStateRef.enableRawEditing() - imageEditorDialog.updatePulse++ + visible: (imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.hasRaw : false) && imageEditorDialog.sourceExpanded + opacity: 0.65 + spacing: 8 + + Button { + text: (imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.isRawActive) ? "RAW Loaded" : "Load RAW" + Layout.fillWidth: true + Layout.preferredHeight: imageEditorDialog.secondaryButtonHeight + font.pixelSize: 12 + enabled: imageEditorDialog.uiStateRef ? !imageEditorDialog.uiStateRef.isRawActive : false + onClicked: { + if (imageEditorDialog.uiStateRef) imageEditorDialog.uiStateRef.enableRawEditing() + imageEditorDialog.updatePulse++ + } } } + Label { text: imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.saveBehaviorMessage : "" + visible: text.length > 0 Layout.fillWidth: true wrapMode: Text.WordWrap font.pixelSize: 11 @@ -281,14 +375,14 @@ Window { } Loader { sourceComponent: sectionSeparator - visible: imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.hasRaw : false + visible: (imageEditorDialog.uiStateRef ? imageEditorDialog.uiStateRef.hasRaw : false) && imageEditorDialog.sourceExpanded } // --- Color Group --- Loader { sourceComponent: sectionHeader Layout.topMargin: (imageEditorDialog.uiStateRef && imageEditorDialog.uiStateRef.hasRaw) ? 5 : 0 // Adjust logic if needed - onLoaded: item.text = "🎨 Color" + onLoaded: item.text = "COLOR" } ListModel { id: colorModel @@ -306,6 +400,7 @@ Window { id: autoWbButton text: "Auto WB" Layout.fillWidth: true + Layout.preferredHeight: imageEditorDialog.secondaryButtonHeight font.pixelSize: 12 onClicked: { if (imageEditorDialog.controllerRef) imageEditorDialog.controllerRef.auto_white_balance() @@ -316,6 +411,7 @@ Window { id: autoLevelsButton text: "Auto Levels" Layout.fillWidth: true + Layout.preferredHeight: imageEditorDialog.secondaryButtonHeight font.pixelSize: 12 onClicked: { if (imageEditorDialog.controllerRef) imageEditorDialog.controllerRef.auto_levels() @@ -329,7 +425,7 @@ Window { // --- Effects Group --- Loader { sourceComponent: sectionHeader - onLoaded: item.text = "✨ Effects" + onLoaded: item.text = "EFFECTS" } ListModel { id: effectsModel @@ -364,7 +460,7 @@ Window { // --- Transform Group --- Loader { sourceComponent: sectionHeader - onLoaded: item.text = "🔄 Transform" + onLoaded: item.text = "TRANSFORM" } RowLayout { Layout.fillWidth: true @@ -400,15 +496,15 @@ Window { text: "Reset" flat: true Layout.preferredWidth: 80 - Material.foreground: imageEditorDialog.textColor + Material.foreground: "#6b6764" onClicked: { if (imageEditorDialog.controllerRef) imageEditorDialog.controllerRef.reset_edit_parameters() imageEditorDialog.updatePulse++ } background: Rectangle { - color: resetButton.down ? "#20ffffff" : "transparent" + color: "transparent" radius: 4 - border.color: resetButton.hovered ? "#40ffffff" : "transparent" + border.color: "transparent" } } @@ -431,9 +527,10 @@ Window { verticalAlignment: Text.AlignVCenter } background: Rectangle { - color: closeEditorButton.down ? "#40ffffff" : "#20ffffff" + color: closeEditorButton.down ? "#20ffffff" : "transparent" radius: 4 - border.color: closeEditorButton.hovered ? "#60ffffff" : "transparent" + border.color: closeEditorButton.hovered ? "#60ffffff" : imageEditorDialog.controlBorder + border.width: 1 } } @@ -608,8 +705,8 @@ Window { Rectangle { anchors.fill: parent radius: 3 - color: imageEditorDialog.controlBg - border.color: imageEditorDialog.controlBorder + color: "#2e2e2e" + border.color: "#383838" border.width: 1 } @@ -639,9 +736,9 @@ Window { width: 12 height: 12 radius: 6 - color: slider.pressed ? imageEditorDialog.accentColor : "white" - border.color: slider.pressed ? "white" : imageEditorDialog.accentColor - border.width: 2 + color: "#e8e6e3" + border.color: slider.pressed ? imageEditorDialog.accentColor : "#5a5755" + border.width: 1 // Glow/Scale effect on hover scale: hoverHandler.hovered || slider.pressed ? 1.3 : 1.0 @@ -654,96 +751,27 @@ Window { } } - // Refined SpinBox - SpinBox { - id: valueInput - from: sliderRow.minVal - to: sliderRow.maxVal - stepSize: 1 - editable: true - Layout.preferredWidth: 80 - Layout.alignment: Qt.AlignVCenter - - value: sliderRow.isReversed ? -slider.value : slider.value - - onValueModified: { - var val = value - var sendValue = sliderRow.isReversed ? -val : val - if (imageEditorDialog.controllerRef) imageEditorDialog.controllerRef.set_edit_parameter(sliderRow.key, sendValue / sliderRow.maxVal) - imageEditorDialog.updatePulse++ - } - - contentItem: TextInput { - z: 2 - text: valueInput.displayText - font.pixelSize: 12 - font.family: valueInput.font.family - color: imageEditorDialog.textColor - selectionColor: imageEditorDialog.accentColor - selectedTextColor: "#ffffff" - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - readOnly: !valueInput.editable - validator: valueInput.validator - inputMethodHints: Qt.ImhFormattedNumbersOnly - - // Highlight on focus - onActiveFocusChanged: { - if(activeFocus) valueInputBackground.border.color = imageEditorDialog.accentColor - else valueInputBackground.border.color = imageEditorDialog.controlBorder - } - } - - up.indicator: Item { - x: valueInput.mirrored ? 0 : parent.width - width - height: parent.height - width: 16 // Smaller button - - Rectangle { - anchors.centerIn: parent - width: 16; height: 16 - radius: 2 - color: valueInput.up.pressed ? imageEditorDialog.accentColor : (valueInput.up.hovered ? Qt.lighter(imageEditorDialog.controlBg, 1.5) : "transparent") - - Text { - text: "+" - font.pixelSize: 12 - anchors.centerIn: parent - color: valueInput.up.pressed ? "white" : imageEditorDialog.textColor - } - } - } + Text { + id: valueReadout + property int displayValue: Math.round(sliderRow.isReversed ? -slider.value : slider.value) - down.indicator: Item { - x: valueInput.mirrored ? parent.width - width : 0 - height: parent.height - width: 16 // Smaller button - - Rectangle { - anchors.centerIn: parent - width: 16; height: 16 - radius: 2 - color: valueInput.down.pressed ? imageEditorDialog.accentColor : (valueInput.down.hovered ? Qt.lighter(imageEditorDialog.controlBg, 1.5) : "transparent") - - Text { - text: "-" - font.pixelSize: 12 - anchors.centerIn: parent - color: valueInput.down.pressed ? "white" : imageEditorDialog.textColor - } + text: displayValue === 0 ? "0" : (displayValue > 0 ? "+" + displayValue : "−" + Math.abs(displayValue)) + Layout.preferredWidth: 40 + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.family: "IBM Plex Mono" + font.pixelSize: 11 + color: displayValue === 0 ? "#6b6764" : "#e8e6e3" + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (!slider.isResetting) + slider.triggerReset() } } - - background: Rectangle { - id: valueInputBackground - implicitWidth: 80 - color: "transparent" - border.color: imageEditorDialog.controlBorder - border.width: 1 - radius: 4 - - Behavior on border.color { ColorAnimation { duration: 150 } } - } } } } diff --git a/faststack/qml/OverlaidHistogram.qml b/faststack/qml/OverlaidHistogram.qml new file mode 100644 index 0000000..34baf8f --- /dev/null +++ b/faststack/qml/OverlaidHistogram.qml @@ -0,0 +1,188 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +Item { + id: root + + property var rData: [] + property var gData: [] + property var bData: [] + property int rClip: 0 + property int gClip: 0 + property int bClip: 0 + property int rPreClip: 0 + property int gPreClip: 0 + property int bPreClip: 0 + property color gridLineColor: "#2e2e2e" + + onRDataChanged: canvas.requestPaint() + onGDataChanged: canvas.requestPaint() + onBDataChanged: canvas.requestPaint() + onGridLineColorChanged: canvas.requestPaint() + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Canvas { + id: canvas + + Layout.fillWidth: true + Layout.fillHeight: true + + onAvailableChanged: { if (available) requestPaint() } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + + // Background + ctx.fillStyle = "#0a0a0a" + ctx.fillRect(0, 0, width, height) + + // Grid lines (4 horizontal divisions) + ctx.strokeStyle = root.gridLineColor + ctx.lineWidth = 1 + for (var i = 1; i < 4; i++) { + ctx.beginPath() + ctx.moveTo(0, i * height / 4) + ctx.lineTo(width, i * height / 4) + ctx.stroke() + } + + // Danger zone (rightmost ~2%) + ctx.fillStyle = "rgba(255, 0, 0, 0.12)" + var dz = (250 / 255) * width + ctx.fillRect(dz, 0, width - dz, height) + + // Draw channels: B first (back), G, R (front) + var channels = [ + { data: root.bData, fill: "rgba(60, 80, 220, 0.40)", stroke: "rgba(80, 100, 230, 0.85)" }, + { data: root.gData, fill: "rgba(60, 180, 60, 0.38)", stroke: "rgba(80, 200, 80, 0.80)" }, + { data: root.rData, fill: "rgba(200, 60, 60, 0.50)", stroke: "rgba(220, 80, 80, 0.90)" }, + ] + + for (var ci = 0; ci < channels.length; ci++) { + var ch = channels[ci] + var data = ch.data + if (!data || data.length === 0) continue + + var len = data.length + var maxVal = 0 + for (var j = 0; j < len; j++) maxVal = Math.max(maxVal, data[j]) + if (maxVal === 0) continue + + ctx.beginPath() + ctx.moveTo(0, height) + + if (width >= len) { + for (var k = 0; k < len; k++) { + var x = len > 1 ? (k / (len - 1)) * width : width / 2 + ctx.lineTo(x, height - (data[k] / maxVal) * height) + } + } else { + // Max-pool downsampling (same as SingleChannelHistogram) + for (var px = 0; px < width; px++) { + var binStart = Math.floor((px / width) * len) + var binEnd = Math.ceil(((px + 1) / width) * len) + binStart = Math.max(0, Math.min(len - 1, binStart)) + binEnd = Math.max(binStart + 1, Math.min(len, binEnd)) + var localMax = 0 + for (var b = binStart; b < binEnd; b++) { + if (b < len) localMax = Math.max(localMax, data[b]) + } + ctx.lineTo(px, height - (localMax / maxVal) * height) + } + } + + ctx.lineTo(width, height) + ctx.closePath() + ctx.fillStyle = ch.fill + ctx.fill() + ctx.strokeStyle = ch.stroke + ctx.lineWidth = 1.2 + ctx.stroke() + } + } + } + + // Stat bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 38 + color: "#1a1a1a" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: 10 + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 1 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "R Preclip: " + root.rPreClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + color: "#804040" + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Clipped: " + root.rClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + font.bold: root.rClip > 0 + color: root.rClip > 0 ? "#ff6060" : "#804040" + } + } + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 1 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "G Preclip: " + root.gPreClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + color: "#407040" + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Clipped: " + root.gClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + font.bold: root.gClip > 0 + color: root.gClip > 0 ? "#60ff60" : "#407040" + } + } + + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 1 + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "B Preclip: " + root.bPreClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + color: "#404080" + } + Text { + anchors.horizontalCenter: parent.horizontalCenter + text: "Clipped: " + root.bClip + font.pixelSize: 9 + font.family: "IBM Plex Mono" + font.bold: root.bClip > 0 + color: root.bClip > 0 ? "#8080ff" : "#404080" + } + } + } + } + } +} diff --git a/faststack/qml/SingleChannelHistogram.qml b/faststack/qml/SingleChannelHistogram.qml index ccf3deb..f3924f9 100644 --- a/faststack/qml/SingleChannelHistogram.qml +++ b/faststack/qml/SingleChannelHistogram.qml @@ -11,6 +11,7 @@ Item { property color gridLineColor: "#50ffffff" // Default semi-transparent white property color dangerColor: Qt.rgba(1, 0, 0, 0.25) property color textColor: "white" + readonly property bool compactStats: root.width < 50 // Allow minimal mode (hide text) property bool minimal: false @@ -140,22 +141,23 @@ Item { } } - RowLayout { + ColumnLayout { Layout.alignment: Qt.AlignHCenter - spacing: 5 + spacing: 0 visible: !root.minimal && root.height > 80 Text { - text: "P:" + root.preClipCount + text: (root.compactStats ? "P: " : "Pre-clip: ") + root.preClipCount color: root.textColor font.pixelSize: Math.max(8, Math.min(11, root.height / 15)) - visible: root.width > 120 + Layout.alignment: Qt.AlignHCenter } Text { - text: (root.width > 120 ? "Clipped: " : "C:") + root.clipCount + text: (root.compactStats ? "C: " : "Clipped: ") + root.clipCount color: root.clipCount > 0 ? "red" : root.textColor font.bold: root.clipCount > 0 font.pixelSize: Math.max(8, Math.min(11, root.height / 15)) + Layout.alignment: Qt.AlignHCenter } } } diff --git a/pyproject.toml b/pyproject.toml index cb86782..f156b2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", ] dependencies = [ - "PySide6>=6.0,<7.0", + "PySide6>=6.5,<7.0", "PyTurboJPEG>=1.8,<2.0", "numpy>=2.0,<3.0", "cachetools>=5.0,<6.0",