1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Parameter SpinBox, a custom Qt widget 5# Copyright (C) 2011-2019 Filipe Coelho <falktx@falktx.com> 6# 7# This program is free software; you can redistribute it and/or 8# modify it under the terms of the GNU General Public License as 9# published by the Free Software Foundation; either version 2 of 10# the License, or any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# For a full copy of the GNU General Public License see the doc/GPL.txt file. 18 19# ------------------------------------------------------------------------------------------------------------ 20# Imports (Global) 21 22from math import isnan, modf 23from random import random 24 25from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer 26from PyQt5.QtGui import QCursor, QPalette 27from PyQt5.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar 28 29# ------------------------------------------------------------------------------------------------------------ 30# Imports (Custom) 31 32import ui_inputdialog_value 33 34from carla_shared import countDecimalPoints, MACOS 35 36# ------------------------------------------------------------------------------------------------------------ 37# Get a fixed value within min/max bounds 38 39def geFixedValue(name, value, minimum, maximum): 40 if isnan(value): 41 print("Parameter '%s' is NaN! - %f" % (name, value)) 42 return minimum 43 if value < minimum: 44 print("Parameter '%s' too low! - %f/%f" % (name, value, minimum)) 45 return minimum 46 if value > maximum: 47 print("Parameter '%s' too high! - %f/%f" % (name, value, maximum)) 48 return maximum 49 return value 50 51# ------------------------------------------------------------------------------------------------------------ 52# Custom InputDialog with Scale Points support 53 54class CustomInputDialog(QDialog): 55 def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix): 56 QDialog.__init__(self, parent) 57 self.ui = ui_inputdialog_value.Ui_Dialog() 58 self.ui.setupUi(self) 59 60 decimals = countDecimalPoints(step, stepSmall) 61 self.ui.label.setText(label) 62 self.ui.doubleSpinBox.setDecimals(decimals) 63 self.ui.doubleSpinBox.setRange(minimum, maximum) 64 self.ui.doubleSpinBox.setSingleStep(step) 65 self.ui.doubleSpinBox.setValue(current) 66 self.ui.doubleSpinBox.setPrefix(prefix) 67 self.ui.doubleSpinBox.setSuffix(suffix) 68 69 if MACOS: 70 self.setWindowModality(Qt.WindowModal) 71 72 if not scalePoints: 73 self.ui.groupBox.setVisible(False) 74 self.resize(200, 0) 75 else: 76 text = "<table>" 77 for scalePoint in scalePoints: 78 valuestr = ("%i" if decimals == 0 else "%f") % scalePoint['value'] 79 text += "<tr><td align='right'>%s</td><td align='left'> - %s</td></tr>" % (valuestr, scalePoint['label']) 80 text += "</table>" 81 self.ui.textBrowser.setText(text) 82 self.resize(200, 300) 83 84 self.fRetValue = current 85 self.adjustSize() 86 self.accepted.connect(self.slot_setReturnValue) 87 88 def returnValue(self): 89 return self.fRetValue 90 91 @pyqtSlot() 92 def slot_setReturnValue(self): 93 self.fRetValue = self.ui.doubleSpinBox.value() 94 95 def done(self, r): 96 QDialog.done(self, r) 97 self.close() 98 99# ------------------------------------------------------------------------------------------------------------ 100# ProgressBar used for ParamSpinBox 101 102class ParamProgressBar(QProgressBar): 103 # signals 104 dragStateChanged = pyqtSignal(bool) 105 valueChanged = pyqtSignal(float) 106 107 def __init__(self, parent): 108 QProgressBar.__init__(self, parent) 109 110 self.fLeftClickDown = False 111 self.fIsInteger = False 112 self.fIsReadOnly = False 113 114 self.fMinimum = 0.0 115 self.fMaximum = 1.0 116 self.fInitiated = False 117 self.fRealValue = 0.0 118 119 self.fLastPaintedValue = None 120 self.fCurrentPaintedText = "" 121 122 self.fName = "" 123 self.fLabelPrefix = "" 124 self.fLabelSuffix = "" 125 self.fTextCall = None 126 self.fValueCall = None 127 128 self.setFormat("(none)") 129 130 # Fake internal value, 10'000 precision 131 QProgressBar.setMinimum(self, 0) 132 QProgressBar.setMaximum(self, 10000) 133 QProgressBar.setValue(self, 0) 134 135 def setMinimum(self, value): 136 self.fMinimum = value 137 138 def setMaximum(self, value): 139 self.fMaximum = value 140 141 def setValue(self, value): 142 if (self.fRealValue == value or isnan(value)) and self.fInitiated: 143 return False 144 145 self.fInitiated = True 146 self.fRealValue = value 147 div = float(self.fMaximum - self.fMinimum) 148 149 if div == 0.0: 150 print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName, 151 value, 152 self.fMaximum, 153 self.fMinimum)) 154 vper = 1.0 155 elif isnan(value): 156 print("Parameter '%s' is NaN (value:%f, min:%f, max:%f)" % (self.fName, 157 value, 158 self.fMaximum, 159 self.fMinimum)) 160 vper = 1.0 161 else: 162 vper = float(value - self.fMinimum) / div 163 164 if vper < 0.0: 165 vper = 0.0 166 elif vper > 1.0: 167 vper = 1.0 168 169 if self.fValueCall is not None: 170 self.fValueCall(value) 171 172 QProgressBar.setValue(self, int(vper * 10000)) 173 return True 174 175 def setSuffixes(self, prefix, suffix): 176 self.fLabelPrefix = prefix 177 self.fLabelSuffix = suffix 178 179 # force refresh of text value 180 self.fLastPaintedValue = None 181 182 self.update() 183 184 def setName(self, name): 185 self.fName = name 186 187 def setReadOnly(self, yesNo): 188 self.fIsReadOnly = yesNo 189 190 def setTextCall(self, textCall): 191 self.fTextCall = textCall 192 193 def setValueCall(self, valueCall): 194 self.fValueCall = valueCall 195 196 def handleMouseEventPos(self, pos): 197 if self.fIsReadOnly: 198 return 199 200 xper = float(pos.x()) / float(self.width()) 201 value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum 202 203 if self.fIsInteger: 204 value = round(value) 205 206 if value < self.fMinimum: 207 value = self.fMinimum 208 elif value > self.fMaximum: 209 value = self.fMaximum 210 211 if self.setValue(value): 212 self.valueChanged.emit(value) 213 214 def mousePressEvent(self, event): 215 if self.fIsReadOnly: 216 return 217 218 if event.button() == Qt.LeftButton: 219 self.handleMouseEventPos(event.pos()) 220 self.fLeftClickDown = True 221 self.dragStateChanged.emit(True) 222 else: 223 self.fLeftClickDown = False 224 225 QProgressBar.mousePressEvent(self, event) 226 227 def mouseMoveEvent(self, event): 228 if self.fIsReadOnly: 229 return 230 231 if self.fLeftClickDown: 232 self.handleMouseEventPos(event.pos()) 233 234 QProgressBar.mouseMoveEvent(self, event) 235 236 def mouseReleaseEvent(self, event): 237 if self.fIsReadOnly: 238 return 239 240 self.fLeftClickDown = False 241 self.dragStateChanged.emit(False) 242 QProgressBar.mouseReleaseEvent(self, event) 243 244 def paintEvent(self, event): 245 if self.fTextCall is not None: 246 if self.fLastPaintedValue != self.fRealValue: 247 self.fLastPaintedValue = self.fRealValue 248 self.fCurrentPaintedText = self.fTextCall() 249 self.setFormat("%s%s%s" % (self.fLabelPrefix, self.fCurrentPaintedText, self.fLabelSuffix)) 250 251 elif self.fIsInteger: 252 self.setFormat("%s%i%s" % (self.fLabelPrefix, int(self.fRealValue), self.fLabelSuffix)) 253 254 else: 255 self.setFormat("%s%f%s" % (self.fLabelPrefix, self.fRealValue, self.fLabelSuffix)) 256 257 QProgressBar.paintEvent(self, event) 258 259# ------------------------------------------------------------------------------------------------------------ 260# Special SpinBox used for parameters 261 262class ParamSpinBox(QAbstractSpinBox): 263 # signals 264 valueChanged = pyqtSignal(float) 265 266 def __init__(self, parent): 267 QAbstractSpinBox.__init__(self, parent) 268 269 self.fName = "" 270 self.fLabelPrefix = "" 271 self.fLabelSuffix = "" 272 273 self.fMinimum = 0.0 274 self.fMaximum = 1.0 275 self.fDefault = 0.0 276 self.fValue = None 277 278 self.fStep = 0.01 279 self.fStepSmall = 0.0001 280 self.fStepLarge = 0.1 281 282 self.fIsReadOnly = False 283 self.fScalePoints = None 284 self.fUseScalePoints = False 285 286 self.fBar = ParamProgressBar(self) 287 self.fBar.setContextMenuPolicy(Qt.NoContextMenu) 288 #self.fBar.show() 289 290 barPalette = self.fBar.palette() 291 barPalette.setColor(QPalette.Window, Qt.transparent) 292 self.fBar.setPalette(barPalette) 293 294 self.fBox = None 295 296 self.lineEdit().hide() 297 298 self.customContextMenuRequested.connect(self.slot_showCustomMenu) 299 self.fBar.valueChanged.connect(self.slot_progressBarValueChanged) 300 301 self.dragStateChanged = self.fBar.dragStateChanged 302 303 QTimer.singleShot(0, self.slot_updateProgressBarGeometry) 304 305 def setDefault(self, value): 306 value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum) 307 self.fDefault = value 308 309 def setMinimum(self, value): 310 self.fMinimum = value 311 self.fBar.setMinimum(value) 312 313 def setMaximum(self, value): 314 self.fMaximum = value 315 self.fBar.setMaximum(value) 316 317 def setValue(self, value): 318 if not self.fIsReadOnly: 319 value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum) 320 321 if self.fBar.fIsInteger: 322 value = round(value) 323 324 if self.fValue == value: 325 return False 326 327 self.fValue = value 328 self.fBar.setValue(value) 329 330 if self.fUseScalePoints: 331 self._setScalePointValue(value) 332 333 self.valueChanged.emit(value) 334 self.update() 335 336 return True 337 338 def setStep(self, value): 339 if value == 0.0: 340 self.fStep = 0.001 341 else: 342 self.fStep = value 343 344 if self.fStepSmall > value: 345 self.fStepSmall = value 346 if self.fStepLarge < value: 347 self.fStepLarge = value 348 349 self.fBar.fIsInteger = bool(self.fStepSmall == 1.0) 350 351 def setStepSmall(self, value): 352 if value == 0.0: 353 self.fStepSmall = 0.0001 354 elif value > self.fStep: 355 self.fStepSmall = self.fStep 356 else: 357 self.fStepSmall = value 358 359 self.fBar.fIsInteger = bool(self.fStepSmall == 1.0) 360 361 def setStepLarge(self, value): 362 if value == 0.0: 363 self.fStepLarge = 0.1 364 elif value < self.fStep: 365 self.fStepLarge = self.fStep 366 else: 367 self.fStepLarge = value 368 369 def setLabel(self, label): 370 prefix = "" 371 suffix = label.strip() 372 373 if suffix == "(coef)": 374 prefix = "* " 375 suffix = "" 376 else: 377 suffix = " " + suffix 378 379 self.fLabelPrefix = prefix 380 self.fLabelSuffix = suffix 381 self.fBar.setSuffixes(prefix, suffix) 382 383 def setName(self, name): 384 self.fName = name 385 self.fBar.setName(name) 386 387 def setTextCallback(self, textCall): 388 self.fBar.setTextCall(textCall) 389 390 def setValueCallback(self, valueCall): 391 self.fBar.setValueCall(valueCall) 392 393 def setReadOnly(self, yesNo): 394 self.fIsReadOnly = yesNo 395 self.fBar.setReadOnly(yesNo) 396 self.setButtonSymbols(QAbstractSpinBox.UpDownArrows if yesNo else QAbstractSpinBox.NoButtons) 397 QAbstractSpinBox.setReadOnly(self, yesNo) 398 399 # FIXME use change event 400 def setEnabled(self, yesNo): 401 self.fBar.setEnabled(yesNo) 402 QAbstractSpinBox.setEnabled(self, yesNo) 403 404 def setScalePoints(self, scalePoints, useScalePoints): 405 if len(scalePoints) == 0: 406 self.fScalePoints = None 407 self.fUseScalePoints = False 408 return 409 410 self.fScalePoints = scalePoints 411 self.fUseScalePoints = useScalePoints 412 413 if not useScalePoints: 414 return 415 416 # Hide ProgressBar and create a ComboBox 417 self.fBar.close() 418 self.fBox = QComboBox(self) 419 self.fBox.setContextMenuPolicy(Qt.NoContextMenu) 420 #self.fBox.show() 421 self.slot_updateProgressBarGeometry() 422 423 # Add items, sorted 424 boxItemValues = [] 425 426 for scalePoint in scalePoints: 427 value = scalePoint['value'] 428 429 if self.fStep == 1.0: 430 label = "%i - %s" % (int(value), scalePoint['label']) 431 else: 432 label = "%f - %s" % (value, scalePoint['label']) 433 434 if len(boxItemValues) == 0: 435 self.fBox.addItem(label) 436 boxItemValues.append(value) 437 438 else: 439 if value < boxItemValues[0]: 440 self.fBox.insertItem(0, label) 441 boxItemValues.insert(0, value) 442 elif value > boxItemValues[-1]: 443 self.fBox.addItem(label) 444 boxItemValues.append(value) 445 else: 446 for index in range(len(boxItemValues)): 447 if value >= boxItemValues[index]: 448 self.fBox.insertItem(index+1, label) 449 boxItemValues.insert(index+1, value) 450 break 451 452 if self.fValue is not None: 453 self._setScalePointValue(self.fValue) 454 455 self.fBox.currentIndexChanged['QString'].connect(self.slot_comboBoxIndexChanged) 456 457 def setToolTip(self, text): 458 self.fBar.setToolTip(text) 459 QAbstractSpinBox.setToolTip(self, text) 460 461 def stepBy(self, steps): 462 if steps == 0 or self.fValue is None: 463 return 464 465 value = self.fValue + (self.fStep * steps) 466 467 if value < self.fMinimum: 468 value = self.fMinimum 469 elif value > self.fMaximum: 470 value = self.fMaximum 471 472 self.setValue(value) 473 474 def stepEnabled(self): 475 if self.fIsReadOnly or self.fValue is None: 476 return QAbstractSpinBox.StepNone 477 if self.fValue <= self.fMinimum: 478 return QAbstractSpinBox.StepUpEnabled 479 if self.fValue >= self.fMaximum: 480 return QAbstractSpinBox.StepDownEnabled 481 return (QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled) 482 483 def updateAll(self): 484 self.update() 485 self.fBar.update() 486 if self.fBox is not None: 487 self.fBox.update() 488 489 def resizeEvent(self, event): 490 QAbstractSpinBox.resizeEvent(self, event) 491 self.slot_updateProgressBarGeometry() 492 493 @pyqtSlot(str) 494 def slot_comboBoxIndexChanged(self, boxText): 495 if self.fIsReadOnly: 496 return 497 498 value = float(boxText.split(" - ", 1)[0]) 499 lastScaleValue = self.fScalePoints[-1]['value'] 500 501 if value == lastScaleValue: 502 value = self.fMaximum 503 504 self.setValue(value) 505 506 @pyqtSlot(float) 507 def slot_progressBarValueChanged(self, value): 508 if self.fIsReadOnly: 509 return 510 511 if value <= self.fMinimum: 512 realValue = self.fMinimum 513 elif value >= self.fMaximum: 514 realValue = self.fMaximum 515 else: 516 curStep = int((value - self.fMinimum) / self.fStep + 0.5) 517 realValue = self.fMinimum + (self.fStep * curStep) 518 519 if realValue < self.fMinimum: 520 realValue = self.fMinimum 521 elif realValue > self.fMaximum: 522 realValue = self.fMaximum 523 524 self.setValue(realValue) 525 526 @pyqtSlot() 527 def slot_showCustomMenu(self): 528 clipboard = QApplication.instance().clipboard() 529 pasteText = clipboard.text() 530 pasteValue = None 531 532 if pasteText: 533 try: 534 pasteValue = float(pasteText) 535 except: 536 pass 537 538 menu = QMenu(self) 539 actReset = menu.addAction(self.tr("Reset (%f)" % self.fDefault)) 540 actRandom = menu.addAction(self.tr("Random")) 541 menu.addSeparator() 542 actCopy = menu.addAction(self.tr("Copy (%f)" % self.fValue)) 543 544 if pasteValue is None: 545 actPaste = menu.addAction(self.tr("Paste")) 546 actPaste.setEnabled(False) 547 else: 548 actPaste = menu.addAction(self.tr("Paste (%f)" % pasteValue)) 549 550 menu.addSeparator() 551 552 actSet = menu.addAction(self.tr("Set value...")) 553 554 if self.fIsReadOnly: 555 actReset.setEnabled(False) 556 actRandom.setEnabled(False) 557 actPaste.setEnabled(False) 558 actSet.setEnabled(False) 559 560 actSel = menu.exec_(QCursor.pos()) 561 562 if actSel == actReset: 563 self.setValue(self.fDefault) 564 565 elif actSel == actRandom: 566 value = random() * (self.fMaximum - self.fMinimum) + self.fMinimum 567 self.setValue(value) 568 569 elif actSel == actCopy: 570 clipboard.setText("%f" % self.fValue) 571 572 elif actSel == actPaste: 573 self.setValue(pasteValue) 574 575 elif actSel == actSet: 576 dialog = CustomInputDialog(self, self.fName, self.fValue, self.fMinimum, self.fMaximum, 577 self.fStep, self.fStepSmall, self.fScalePoints, 578 self.fLabelPrefix, self.fLabelSuffix) 579 if dialog.exec_(): 580 value = dialog.returnValue() 581 self.setValue(value) 582 583 @pyqtSlot() 584 def slot_updateProgressBarGeometry(self): 585 geometry = self.lineEdit().geometry() 586 dx = geometry.x()-1 587 dy = geometry.y()-1 588 geometry.adjust(-dx, -dy, dx, dy) 589 self.fBar.setGeometry(geometry) 590 if self.fUseScalePoints: 591 self.fBox.setGeometry(geometry) 592 593 def _getNearestScalePoint(self, realValue): 594 finalValue = 0.0 595 596 for i in range(len(self.fScalePoints)): 597 scaleValue = self.fScalePoints[i]["value"] 598 if i == 0: 599 finalValue = scaleValue 600 else: 601 srange1 = abs(realValue - scaleValue) 602 srange2 = abs(realValue - finalValue) 603 604 if srange2 > srange1: 605 finalValue = scaleValue 606 607 return finalValue 608 609 def _setScalePointValue(self, value): 610 value = self._getNearestScalePoint(value) 611 612 for i in range(self.fBox.count()): 613 if float(self.fBox.itemText(i).split(" - ", 1)[0]) == value: 614 self.fBox.setCurrentIndex(i) 615 break 616