|  | #!/usr/bin/env python
# -*- coding: utf-8 -*-
# Parameter SpinBox, a custom Qt widget
# Copyright (C) 2011-2019 Filipe Coelho <falktx@falktx.com>
#
# 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 2 of
# the License, or 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.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.
# ------------------------------------------------------------------------------------------------------------
# Imports (Global)
from math import isnan, modf
from random import random
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer
from PyQt5.QtGui import QCursor, QPalette
from PyQt5.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar
# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)
import ui_inputdialog_value
# ------------------------------------------------------------------------------------------------------------
# Get a fixed value within min/max bounds
def geFixedValue(name, value, minimum, maximum):
    if isnan(value):
        print("Parameter '%s' is NaN! - %f" % (name, value))
        return minimum
    if value < minimum:
        print("Parameter '%s' too low! - %f/%f" % (name, value, minimum))
        return minimum
    if value > maximum:
        print("Parameter '%s' too high! - %f/%f" % (name, value, maximum))
        return maximum
    return value
# ------------------------------------------------------------------------------------------------------------
# Custom InputDialog with Scale Points support
class CustomInputDialog(QDialog):
    def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints):
        QDialog.__init__(self, parent)
        self.ui = ui_inputdialog_value.Ui_Dialog()
        self.ui.setupUi(self)
        # calculate num decimals from stepSmall
        if stepSmall >= 1.0:
            decimals = 0
        elif step >= 1.0:
            decimals = 2
        else:
            decfrac, decwhole = modf(stepSmall)
            if "000" in str(decfrac):
                decfrac = round(decfrac, str(decfrac).find("000"))
            else:
                decfrac = round(decfrac, 12)
            decimals = abs(len(str(decfrac))-len(str(decwhole))-1)
        self.ui.label.setText(label)
        self.ui.doubleSpinBox.setDecimals(decimals)
        self.ui.doubleSpinBox.setRange(minimum, maximum)
        self.ui.doubleSpinBox.setSingleStep(step)
        self.ui.doubleSpinBox.setValue(current)
        if not scalePoints:
            self.ui.groupBox.setVisible(False)
            self.resize(200, 0)
        else:
            text = "<table>"
            for scalePoint in scalePoints:
                text += "<tr><td align='right'>%f</td><td align='left'> - %s</td></tr>" % (scalePoint['value'], scalePoint['label'])
            text += "</table>"
            self.ui.textBrowser.setText(text)
            self.resize(200, 300)
        self.fRetValue = current
        self.accepted.connect(self.slot_setReturnValue)
    def returnValue(self):
        return self.fRetValue
    @pyqtSlot()
    def slot_setReturnValue(self):
        self.fRetValue = self.ui.doubleSpinBox.value()
    def done(self, r):
        QDialog.done(self, r)
        self.close()
# ------------------------------------------------------------------------------------------------------------
# ProgressBar used for ParamSpinBox
class ParamProgressBar(QProgressBar):
    # signals
    dragStateChanged = pyqtSignal(bool)
    valueChanged = pyqtSignal(float)
    def __init__(self, parent):
        QProgressBar.__init__(self, parent)
        self.fLeftClickDown = False
        self.fIsInteger     = False
        self.fIsReadOnly    = False
        self.fMinimum   = 0.0
        self.fMaximum   = 1.0
        self.fInitiated = False
        self.fRealValue = 0.0
        self.fLastPaintedValue   = None
        self.fCurrentPaintedText = ""
        self.fLabel = ""
        self.fName  = ""
        self.fPreLabel = " "
        self.fTextCall  = None
        self.fValueCall = None
        self.setFormat("(none)")
        # Fake internal value, 10'000 precision
        QProgressBar.setMinimum(self, 0)
        QProgressBar.setMaximum(self, 10000)
        QProgressBar.setValue(self, 0)
    def setMinimum(self, value):
        self.fMinimum = value
    def setMaximum(self, value):
        self.fMaximum = value
    def setValue(self, value):
        if self.fRealValue == value and self.fInitiated:
            return False
        self.fInitiated = True
        self.fRealValue = value
        div = float(self.fMaximum - self.fMinimum)
        if div == 0.0:
            print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName, value, self.fMaximum, self.fMinimum))
            vper = 1.0
        else:
            vper = float(value - self.fMinimum) / div
            if vper < 0.0:
                vper = 0.0
            elif vper > 1.0:
                vper = 1.0
        if self.fValueCall is not None:
            self.fValueCall(value)
        QProgressBar.setValue(self, int(vper * 10000))
        return True
    def setLabel(self, label):
        self.fLabel = label.strip()
        if self.fLabel == "(coef)":
            self.fLabel = ""
            self.fPreLabel = "*"
        # force refresh of text value
        self.fLastPaintedValue = None
        self.update()
    def setName(self, name):
        self.fName = name
    def setReadOnly(self, yesNo):
        self.fIsReadOnly = yesNo
    def setTextCall(self, textCall):
        self.fTextCall = textCall
    def setValueCall(self, valueCall):
        self.fValueCall = valueCall
    def handleMouseEventPos(self, pos):
        if self.fIsReadOnly:
            return
        xper  = float(pos.x()) / float(self.width())
        value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
        if self.fIsInteger:
            value = round(value)
        if value < self.fMinimum:
            value = self.fMinimum
        elif value > self.fMaximum:
            value = self.fMaximum
        if self.setValue(value):
            self.valueChanged.emit(value)
    def mousePressEvent(self, event):
        if self.fIsReadOnly:
            return
        if event.button() == Qt.LeftButton:
            self.handleMouseEventPos(event.pos())
            self.fLeftClickDown = True
            self.dragStateChanged.emit(True)
        else:
            self.fLeftClickDown = False
        QProgressBar.mousePressEvent(self, event)
    def mouseMoveEvent(self, event):
        if self.fIsReadOnly:
            return
        if self.fLeftClickDown:
            self.handleMouseEventPos(event.pos())
        QProgressBar.mouseMoveEvent(self, event)
    def mouseReleaseEvent(self, event):
        if self.fIsReadOnly:
            return
        self.fLeftClickDown = False
        self.dragStateChanged.emit(False)
        QProgressBar.mouseReleaseEvent(self, event)
    def paintEvent(self, event):
        if self.fTextCall is not None:
            if self.fLastPaintedValue != self.fRealValue:
                self.fLastPaintedValue   = self.fRealValue
                self.fCurrentPaintedText = self.fTextCall()
            self.setFormat("%s %s %s" % (self.fPreLabel, self.fCurrentPaintedText, self.fLabel))
        elif self.fIsInteger:
            self.setFormat("%s %i %s" % (self.fPreLabel, int(self.fRealValue), self.fLabel))
        else:
            self.setFormat("%s %f %s" % (self.fPreLabel, self.fRealValue, self.fLabel))
        QProgressBar.paintEvent(self, event)
# ------------------------------------------------------------------------------------------------------------
# Special SpinBox used for parameters
class ParamSpinBox(QAbstractSpinBox):
    # signals
    valueChanged = pyqtSignal(float)
    def __init__(self, parent):
        QAbstractSpinBox.__init__(self, parent)
        self.fName = ""
        self.fMinimum = 0.0
        self.fMaximum = 1.0
        self.fDefault = 0.0
        self.fValue   = None
        self.fStep      = 0.01
        self.fStepSmall = 0.0001
        self.fStepLarge = 0.1
        self.fIsReadOnly = False
        self.fScalePoints = None
        self.fUseScalePoints = False
        self.fBar = ParamProgressBar(self)
        self.fBar.setContextMenuPolicy(Qt.NoContextMenu)
        #self.fBar.show()
        barPalette = self.fBar.palette()
        barPalette.setColor(QPalette.Window, Qt.transparent)
        self.fBar.setPalette(barPalette)
        self.fBox = None
        self.lineEdit().hide()
        self.customContextMenuRequested.connect(self.slot_showCustomMenu)
        self.fBar.valueChanged.connect(self.slot_progressBarValueChanged)
        self.dragStateChanged = self.fBar.dragStateChanged
        QTimer.singleShot(0, self.slot_updateProgressBarGeometry)
    def setDefault(self, value):
        value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
        self.fDefault = value
    def setMinimum(self, value):
        self.fMinimum = value
        self.fBar.setMinimum(value)
    def setMaximum(self, value):
        self.fMaximum = value
        self.fBar.setMaximum(value)
    def setValue(self, value):
        if not self.fIsReadOnly:
            value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
        if self.fBar.fIsInteger:
            value = round(value)
        if self.fValue == value:
            return False
        self.fValue = value
        self.fBar.setValue(value)
        if self.fUseScalePoints:
            self._setScalePointValue(value)
        self.valueChanged.emit(value)
        self.update()
        return True
    def setStep(self, value):
        if value == 0.0:
            self.fStep = 0.001
        else:
            self.fStep = value
        if self.fStepSmall > value:
            self.fStepSmall = value
        if self.fStepLarge < value:
            self.fStepLarge = value
        self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
    def setStepSmall(self, value):
        if value == 0.0:
            self.fStepSmall = 0.0001
        elif value > self.fStep:
            self.fStepSmall = self.fStep
        else:
            self.fStepSmall = value
        self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
    def setStepLarge(self, value):
        if value == 0.0:
            self.fStepLarge = 0.1
        elif value < self.fStep:
            self.fStepLarge = self.fStep
        else:
            self.fStepLarge = value
    def setLabel(self, label):
        self.fBar.setLabel(label)
    def setName(self, name):
        self.fName = name
        self.fBar.setName(name)
    def setTextCallback(self, textCall):
        self.fBar.setTextCall(textCall)
    def setValueCallback(self, valueCall):
        self.fBar.setValueCall(valueCall)
    def setReadOnly(self, yesNo):
        self.fIsReadOnly = yesNo
        self.fBar.setReadOnly(yesNo)
        self.setButtonSymbols(QAbstractSpinBox.UpDownArrows if yesNo else QAbstractSpinBox.NoButtons)
        QAbstractSpinBox.setReadOnly(self, yesNo)
    # FIXME use change event
    def setEnabled(self, yesNo):
        self.fBar.setEnabled(yesNo)
        QAbstractSpinBox.setEnabled(self, yesNo)
    def setScalePoints(self, scalePoints, useScalePoints):
        if len(scalePoints) == 0:
            self.fScalePoints    = None
            self.fUseScalePoints = False
            return
        self.fScalePoints    = scalePoints
        self.fUseScalePoints = useScalePoints
        if not useScalePoints:
            return
        # Hide ProgressBar and create a ComboBox
        self.fBar.close()
        self.fBox = QComboBox(self)
        self.fBox.setContextMenuPolicy(Qt.NoContextMenu)
        #self.fBox.show()
        self.slot_updateProgressBarGeometry()
        # Add items, sorted
        boxItemValues = []
        for scalePoint in scalePoints:
            value = scalePoint['value']
            if self.fStep == 1.0:
                label = "%i - %s" % (int(value), scalePoint['label'])
            else:
                label = "%f - %s" % (value, scalePoint['label'])
            if len(boxItemValues) == 0:
                self.fBox.addItem(label)
                boxItemValues.append(value)
            else:
                if value < boxItemValues[0]:
                    self.fBox.insertItem(0, label)
                    boxItemValues.insert(0, value)
                elif value > boxItemValues[-1]:
                    self.fBox.addItem(label)
                    boxItemValues.append(value)
                else:
                    for index in range(len(boxItemValues)):
                        if value >= boxItemValues[index]:
                            self.fBox.insertItem(index+1, label)
                            boxItemValues.insert(index+1, value)
                            break
        if self.fValue is not None:
            self._setScalePointValue(self.fValue)
        self.fBox.currentIndexChanged['QString'].connect(self.slot_comboBoxIndexChanged)
    def stepBy(self, steps):
        if steps == 0 or self.fValue is None:
            return
        value = self.fValue + (self.fStep * steps)
        if value < self.fMinimum:
            value = self.fMinimum
        elif value > self.fMaximum:
            value = self.fMaximum
        self.setValue(value)
    def stepEnabled(self):
        if self.fIsReadOnly or self.fValue is None:
            return QAbstractSpinBox.StepNone
        if self.fValue <= self.fMinimum:
            return QAbstractSpinBox.StepUpEnabled
        if self.fValue >= self.fMaximum:
            return QAbstractSpinBox.StepDownEnabled
        return (QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled)
    def updateAll(self):
        self.update()
        self.fBar.update()
        if self.fBox is not None:
            self.fBox.update()
    def resizeEvent(self, event):
        QAbstractSpinBox.resizeEvent(self, event)
        self.slot_updateProgressBarGeometry()
    @pyqtSlot(str)
    def slot_comboBoxIndexChanged(self, boxText):
        if self.fIsReadOnly:
            return
        value          = float(boxText.split(" - ", 1)[0])
        lastScaleValue = self.fScalePoints[-1]['value']
        if value == lastScaleValue:
            value = self.fMaximum
        self.setValue(value)
    @pyqtSlot(float)
    def slot_progressBarValueChanged(self, value):
        if self.fIsReadOnly:
            return
        if value <= self.fMinimum:
            realValue = self.fMinimum
        elif value >= self.fMaximum:
            realValue = self.fMaximum
        else:
            curStep   = int((value - self.fMinimum) / self.fStep + 0.5)
            realValue = self.fMinimum + (self.fStep * curStep)
            if realValue < self.fMinimum:
                realValue = self.fMinimum
            elif realValue > self.fMaximum:
                realValue = self.fMaximum
        self.setValue(realValue)
    @pyqtSlot()
    def slot_showCustomMenu(self):
        clipboard  = QApplication.instance().clipboard()
        pasteText  = clipboard.text()
        pasteValue = None
        if pasteText:
            try:
                pasteValue = float(pasteText)
            except:
                pass
        menu      = QMenu(self)
        actReset  = menu.addAction(self.tr("Reset (%f)" % self.fDefault))
        actRandom = menu.addAction(self.tr("Random"))
        menu.addSeparator()
        actCopy   = menu.addAction(self.tr("Copy (%f)" % self.fValue))
        if pasteValue is None:
            actPaste = menu.addAction(self.tr("Paste"))
            actPaste.setEnabled(False)
        else:
            actPaste = menu.addAction(self.tr("Paste (%f)" % pasteValue))
        menu.addSeparator()
        actSet = menu.addAction(self.tr("Set value..."))
        if self.fIsReadOnly:
            actReset.setEnabled(False)
            actRandom.setEnabled(False)
            actPaste.setEnabled(False)
            actSet.setEnabled(False)
        actSel = menu.exec_(QCursor.pos())
        if actSel == actReset:
            self.setValue(self.fDefault)
        elif actSel == actRandom:
            value = random() * (self.fMaximum - self.fMinimum) + self.fMinimum
            self.setValue(value)
        elif actSel == actCopy:
            clipboard.setText("%f" % self.fValue)
        elif actSel == actPaste:
            self.setValue(pasteValue)
        elif actSel == actSet:
            dialog = CustomInputDialog(self, self.fName, self.fValue, self.fMinimum, self.fMaximum,
                                             self.fStep, self.fStepSmall, self.fScalePoints)
            if dialog.exec_():
                value = dialog.returnValue()
                self.setValue(value)
    @pyqtSlot()
    def slot_updateProgressBarGeometry(self):
        geometry = self.lineEdit().geometry()
        dx = geometry.x()-1
        dy = geometry.y()-1
        geometry.adjust(-dx, -dy, dx, dy)
        self.fBar.setGeometry(geometry)
        if self.fUseScalePoints:
            self.fBox.setGeometry(geometry)
    def _getNearestScalePoint(self, realValue):
        finalValue = 0.0
        for i in range(len(self.fScalePoints)):
            scaleValue = self.fScalePoints[i]["value"]
            if i == 0:
                finalValue = scaleValue
            else:
                srange1 = abs(realValue - scaleValue)
                srange2 = abs(realValue - finalValue)
                if srange2 > srange1:
                    finalValue = scaleValue
        return finalValue
    def _setScalePointValue(self, value):
        value = self._getNearestScalePoint(value)
        for i in range(self.fBox.count()):
            if float(self.fBox.itemText(i).split(" - ", 1)[0]) == value:
                self.fBox.setCurrentIndex(i)
                break
 |