#!/usr/bin/env python # -*- coding: utf-8 -*- # Parameter SpinBox, a custom Qt4 widget # Copyright (C) 2011-2014 Filipe Coelho # # 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 (Config) from carla_config import * # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from math import isnan, modf from random import random if config_UseQt5: from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar else: from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer from PyQt4.QtGui import QAbstractSpinBox, QApplication, QComboBox, QCursor, 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 = "" for scalePoint in scalePoints: text += "" % (scalePoint['value'], scalePoint['label']) text += "
%f - %s
" 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 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 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 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() self.fBox = None self.lineEdit().hide() self.customContextMenuRequested.connect(self.slot_showCustomMenu) self.fBar.valueChanged.connect(self.slot_progressBarValueChanged) 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): self.fBar.setGeometry(self.lineEdit().geometry()) if self.fUseScalePoints: self.fBox.setGeometry(self.lineEdit().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