/* * Copyright (c) 2014-2015 Meltytech, LLC * Author: Brian Matherly * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.1 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.0 import Shotcut.Controls 1.0 Item { property var defaultParameters: ['left', 'right', 'top', 'bottom', 'center', 'center_bias'] width: 350 height: 200 // Austin: these constants simulate producer.width and producer.height readonly property int producer_width: 3840 readonly property int producer_height: 2160 // Austin: this is the coordinate translation shim that every filter would need function pixelToPercent(type, numerator, denominator) { var pct = 0.0 if (denominator == 0) { return 0.0 } if (type === 'left' || type === 'top' || type === 'width' || type === 'height') { pct = parseFloat(numerator) / denominator } else if (type === 'right' || type === 'bottom') { pct = ((parseFloat(numerator) + 1) / denominator * 65536.0 - 1) / 65536.0 } return pct } // Austin: this avoids rounding creep that could happen from many swaps function recalcScale(percent, dimension, upscale) { if (upscale == true) { return Math.floor(percent * dimension) } else { return Math.ceil(percent * dimension * 100) / 100.0 } } // Austin: this is called on load or profile change to translate pixels to new size function recalcPixelCoords() { if (filter.get('producer_width') == 0) { filter.set('producer_width', producer_width) } // If current width doesn't match cached width, assume a proxy/full swap happened // If they do match, the "proxy math" code is never called and the filter acts // exactly the way it did before proxy code was introduced, full backward compatibility if (producer_width != filter.get('producer_width')) { var upscale = (producer_width > filter.get('producer_width')) ? true : false var pct pct = pixelToPercent('width', filter.get('left'), filter.get('producer_width')) filter.set('left', recalcScale(pct, producer_width, upscale)) pct = pixelToPercent('width', filter.get('right'), filter.get('producer_width')) filter.set('right', recalcScale(pct, producer_width, upscale)) pct = pixelToPercent('width', filter.get('center_bias'), filter.get('producer_width')) filter.set('center_bias', recalcScale(pct, producer_width, upscale)) filter.set('producer_width', producer_width) } if (filter.get('producer_height') == 0) { filter.set('producer_height', producer_height) } // If current height doesn't match cached height, assume a proxy/full swap happened if (producer_height != filter.get('producer_height')) { var upscale = (producer_height > filter.get('producer_height')) ? true : false var pct pct = pixelToPercent('height', filter.get('top'), filter.get('producer_height')) filter.set('top', recalcScale(pct, producer_height, upscale)) pct = pixelToPercent('height', filter.get('bottom'), filter.get('producer_height')) filter.set('bottom', recalcScale(pct, producer_height, upscale)) filter.set('producer_height', producer_height) } } function setEnabled() { if (filter.get('center') == 1) { biasslider.enabled = true biasundo.enabled = true topslider.enabled = false topundo.enabled = false bottomslider.enabled = false bottomundo.enabled = false leftslider.enabled = false leftundo.enabled = false rightslider.enabled = false rightundo.enabled = false } else { biasslider.enabled = false biasundo.enabled = false topslider.enabled = true topundo.enabled = true bottomslider.enabled = true bottomundo.enabled = true leftslider.enabled = true leftundo.enabled = true rightslider.enabled = true rightundo.enabled = true } } Component.onCompleted: { if (filter.isNew) { // Set default parameter values filter.set("center", 0); filter.set("center_bias", 0); filter.set("top", 0); filter.set("bottom", 0); filter.set("left", 0); filter.set("right", 0); centerCheckBox.checked = false filter.savePreset(defaultParameters) } // Austin: Always call immediately after isNew setup recalcPixelCoords() // Austin: to test this, create a project with a clip, add a 2px crop, // save it and exit Shotcut. Edit the .MLT file and change the // producer_width/height to 640x360 as if you had been editing in // proxy mode. Restart Shotcut and open the project and look at // the crop filter. It is hard-coded to 4K and will think a swap // from 360p to 4K happened, and translate the 2px crop into the // 4K equivalent of 12px. // Austin: Then ask the UI to refresh itself to pick up new values biasslider.value = +filter.get('center_bias') topslider.value = +filter.get('top') bottomslider.value = +filter.get('bottom') leftslider.value = +filter.get('left') rightslider.value = +filter.get('right') // Austin: if applicable, would also trap profile.onProfileChanged // and do the same thing: recalcPixelCoords() then a UI refresh // Done! centerCheckBox.checked = filter.get('center') == '1' setEnabled() } GridLayout { columns: 3 anchors.fill: parent anchors.margins: 8 Label { text: qsTr('Preset') Layout.alignment: Qt.AlignRight } Preset { Layout.columnSpan: 2 parameters: defaultParameters onPresetSelected: { centerCheckBox.checked = filter.get('center') == '1' biasslider.value = +filter.get('center_bias') topslider.value = +filter.get('top') bottomslider.value = +filter.get('bottom') leftslider.value = +filter.get('left') rightslider.value = +filter.get('right') setEnabled() } } CheckBox { id: centerCheckBox text: qsTr('Center') checked: filter.get('center') == '1' property bool isReady: false Component.onCompleted: isReady = true onClicked: { if (isReady) { filter.set('center', checked) setEnabled() } } } Item { Layout.fillWidth: true; } UndoButton { onClicked: { centerCheckBox.checked = false filter.set('center', false) setEnabled() } } Label { text: qsTr('Center bias') Layout.alignment: Qt.AlignRight } SliderSpinner { id: biasslider minimumValue: -Math.max(producer_width, producer_height) / 2 maximumValue: Math.max(producer_width, producer_height) / 2 decimals: 2 stepSize: 1 suffix: ' px' value: +filter.get('center_bias') onValueChanged: filter.set('center_bias', value) } UndoButton { id: biasundo onClicked: biasslider.value = 0 } Label { text: qsTr('Top') Layout.alignment: Qt.AlignRight } SliderSpinner { id: topslider minimumValue: 0 maximumValue: producer_height decimals: 2 stepSize: 1 suffix: ' px' value: +filter.get('top') onValueChanged: filter.set('top', value) } UndoButton { id: topundo onClicked: topslider.value = 0 } Label { text: qsTr('Bottom') Layout.alignment: Qt.AlignRight } SliderSpinner { id: bottomslider minimumValue: 0 maximumValue: producer_height decimals: 2 stepSize: 1 suffix: ' px' value: +filter.get('bottom') onValueChanged: filter.set('bottom', value) } UndoButton { id: bottomundo onClicked: bottomslider.value = 0 } Label { text: qsTr('Left') Layout.alignment: Qt.AlignRight } SliderSpinner { id: leftslider minimumValue: 0 maximumValue: producer_width decimals: 2 stepSize: 1 suffix: ' px' value: +filter.get('left') onValueChanged: filter.set('left', value) } UndoButton { id: leftundo onClicked: leftslider.value = 0 } Label { text: qsTr('Right') Layout.alignment: Qt.AlignRight } SliderSpinner { id: rightslider minimumValue: 0 maximumValue: producer_width decimals: 2 stepSize: 1 suffix: ' px' value: +filter.get('right') onValueChanged: filter.set('right', value) } UndoButton { id: rightundo onClicked: rightslider.value = 0 } Item { Layout.fillHeight: true; } } }