Files
linuxcncAllWasm/lib/python/qtvcp/widgets/calculator.py
2026-08-20 06:17:39 +08:00

662 lines
24 KiB
Python

#!/usr/bin/env python3
import math
import sys
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (QApplication, QGridLayout, QLayout, QLineEdit,
QSizePolicy, QPushButton, QDialog, QDialogButtonBox, QMenu, QAction,
QVBoxLayout, QToolButton, QLabel)
from qtpy.QtGui import QDoubleValidator
from qtpy.QtGui import QIcon
from qtpy import QtCore
from qtvcp.core import Status, Info
STATUS = Status()
INFO = Info()
# TODO: this needs to handle internationalization since much of the world uses
# commas instead of periods for decimal points.
class CalculatorLineEdit(QLineEdit):
operatorKeyPressed = QtCore.Signal(str)
digitKeyPressed = QtCore.Signal(str)
fieldKeyPressed = QtCore.Signal(str)
cancelKeyPressed = QtCore.Signal()
def __init__(self, parent=None):
super(CalculatorLineEdit, self).__init__(parent)
self.setMaxLength(15)
self.setAlignment(Qt.AlignRight)
# set up a validator so the input text can only be a number
validator = QDoubleValidator()
# StandardNotation disables scientific notation like 10e-5, which could be left incompleted
# before submitting (e.g. "10e" would be invalid and cause an error)
validator.setNotation(QDoubleValidator.StandardNotation)
self.setValidator(validator)
def keyPressEvent(self, event):
# The digit and decimal keys emit a digitKeyPressed signal
if (event.key() >= Qt.Key_0 and event.key() <= Qt.Key_9) or event.key() == Qt.Key_Period:
self.digitKeyPressed.emit(str(event.text()))
# these keys are absorbed and not sent to QLineEdit but a signal is emitted
# special keys:
# ALT+Left - move to previous field
# ALT+Right - move to next field
# ALT+Backspace - cancel
key_map = {
Qt.Key_Minus: lambda: self.operatorKeyPressed.emit('-'),
Qt.Key_Plus: lambda: self.operatorKeyPressed.emit('+'),
Qt.Key_Asterisk: lambda: self.operatorKeyPressed.emit('*'),
Qt.Key_Slash: lambda: self.operatorKeyPressed.emit('/'),
Qt.Key_Equal: lambda: self.operatorKeyPressed.emit('='),
(Qt.Key_Right, Qt.AltModifier): lambda: self.fieldKeyPressed.emit('next'),
(Qt.Key_Left, Qt.AltModifier): lambda: self.fieldKeyPressed.emit('previous'),
(Qt.Key_Backspace, Qt.AltModifier): self.cancelKeyPressed.emit
}
key = event.key()
modifiers = event.modifiers()
if (key, modifiers) in key_map:
key_map[(key, modifiers)]()
elif key in key_map:
key_map[key]()
else:
# the fallthrough sends everything else to QLineEdit
return super(CalculatorLineEdit, self).keyPressEvent(event)
class Calculator(QDialog):
NumDigitButtons = 10
def __init__(self, parent=None):
super(Calculator, self).__init__(parent)
try:
self.PREFS_ = self.QTVCP_INSTANCE_.PREFS_
self.PREF_SECTION = 'CALCULATOR'
except:
self.PREFS_ = None
self.pendingAdditiveOperator = ''
self.pendingMultiplicativeOperator = ''
self.sumInMemory = 0.0
self.sumSoFar = 0.0
self.factorSoFar = 0.0
self.waitingForOperand = True
self.display = CalculatorLineEdit('0')
self.display.setMinimumHeight(30)
self.display.setAlignment(Qt.AlignRight)
self.display.setMaxLength(15)
self.display.textEdited.connect(self.displayTextEdited)
self.display.textChanged.connect(self.displayTextChanged)
self.pendingLabel = QLabel()
self.pendingLabel.setAlignment(Qt.AlignRight)
self.memoryLabel = QLabel()
self.memoryLabel.setAlignment(Qt.AlignRight)
self.updateMemLabel()
self.display.digitKeyPressed.connect(self.physDigitPressed)
self.display.operatorKeyPressed.connect(self.physOperatorPressed)
self.display.fieldKeyPressed.connect(self.physFieldKeyPressed)
self.display.cancelKeyPressed.connect(self.physCancelKeyPressed)
self.digitButtons = []
for i in range(Calculator.NumDigitButtons):
self.digitButtons.append(self.createButton(str(i),
self.digitClicked))
self.pointButton = self.createButton(".", self.pointClicked)
self.changeSignButton = self.createButton(str("\N{PLUS-MINUS SIGN}"), self.changeSignClicked)
self.axisButton = self.createAxisButton("AXIS X", self.axisClicked)
self.backspaceButton = self.createButton("BKSP",self.backspaceClicked, 'Backspace')
self.clearButton = self.createButton("CLR", self.clear)
self.clearAllButton = self.createButton("CLR ALL", self.clearAll)
self.clearMemoryButton = self.createButton("MC", self.clearMemory, 'Clear memory')
self.readMemoryButton = self.createButton("MR", self.readMemory, 'Read memory')
self.setMemoryButton = self.createButton("MS", self.setMemory, 'Set memory')
self.addToMemoryButton = self.createButton("M+", self.addToMemory, 'Add to memory')
self.divisionButton = self.createButton(str("\N{DIVISION SIGN}"),
self.multiplicativeOperatorClicked)
self.timesButton = self.createButton(str("\N{MULTIPLICATION SIGN}"), self.multiplicativeOperatorClicked)
self.minusButton = self.createButton("-", self.additiveOperatorClicked)
self.plusButton = self.createButton("+", self.additiveOperatorClicked)
self.squareRootButton = self.createButton("SQRT", self.unaryOperatorClicked)
self.powerButton = self.createButton(str("x\N{SUPERSCRIPT TWO}"), self.unaryOperatorClicked)
self.reciprocalButton = self.createButton("1/x", self.unaryOperatorClicked)
self.equalButton = self.createButton("=", self.equalClicked)
self.to_mm_btn = self.createButton('-> MM', self.convertClicked, 'Convert inch to mm')
self.to_mm_btn.setProperty('convert', 'to_mm')
self.to_inch_btn = self.createButton('-> INCH', self.convertClicked, 'Convert mm to inch')
self.to_inch_btn.setProperty('convert', 'to_inch')
self.tpi_btn = self.createButton('25.4/X', self.convertClicked, 'Convert metric pitch to TPI or TPI to metric pitch')
self.tpi_btn.setProperty('convert', '25.4/X')
mainLayout = QGridLayout()
mainLayout.setSizeConstraint(QLayout.SetFixedSize)
mainLayout.addWidget(self.backspaceButton, 2, 0, 1, 1)
mainLayout.addWidget(self.axisButton, 2, 1, 1, 2)
mainLayout.addWidget(self.clearButton, 2, 3, 1, 1)
mainLayout.addWidget(self.clearAllButton, 2, 4, 1, 2)
mainLayout.addWidget(self.clearMemoryButton, 3, 0)
mainLayout.addWidget(self.readMemoryButton, 4, 0)
mainLayout.addWidget(self.setMemoryButton, 5, 0)
mainLayout.addWidget(self.addToMemoryButton, 6, 0)
for i in range(1, Calculator.NumDigitButtons):
row = ((9 - i) // 3) + 2
column = ((i - 1) % 3) + 1
mainLayout.addWidget(self.digitButtons[i], row + 1, column)
mainLayout.addWidget(self.digitButtons[0], 6, 1)
mainLayout.addWidget(self.pointButton, 6, 2)
mainLayout.addWidget(self.changeSignButton, 6, 3)
mainLayout.addWidget(self.divisionButton, 3, 4)
mainLayout.addWidget(self.timesButton, 4, 4)
mainLayout.addWidget(self.minusButton, 5, 4)
mainLayout.addWidget(self.plusButton, 6, 4)
mainLayout.addWidget(self.squareRootButton, 3, 5)
mainLayout.addWidget(self.powerButton, 4, 5)
mainLayout.addWidget(self.reciprocalButton, 5, 5)
mainLayout.addWidget(self.equalButton, 6, 5)
mainLayout.addWidget(self.to_mm_btn, 7, 0)
mainLayout.addWidget(self.to_inch_btn, 7, 1)
mainLayout.addWidget(self.tpi_btn, 7, 2)
if self.PREFS_:
constValues = self.PREFS_.getpref('constValuesList', 'None', str, self.PREF_SECTION)
if constValues != 'None':
self.constButtons = []
constValues = ''.join(constValues.split())
for value in constValues.split(',')[:6]:
constButton = QPushButton(value)
constButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
constButton.setFocusPolicy(Qt.NoFocus)
constButton.clicked.connect(self.constClicked)
mainLayout.addWidget(constButton, len(self.constButtons) + 2, 6)
self.constButtons.append(constButton)
mainLayout.addWidget(self.display, 0, 1, 1, mainLayout.columnCount())
mainLayout.addWidget(self.pendingLabel, 0, 0)
mainLayout.addWidget(self.memoryLabel, 1, 1, 1, mainLayout.columnCount()-1)
self.backButton = QPushButton('Back')
self.backButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.backButton.setFocusPolicy(Qt.NoFocus)
self.backButton.clicked.connect(self.backActionWrapper)
self.nextButton = QPushButton('Next')
self.nextButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.nextButton.setFocusPolicy(Qt.NoFocus)
self.nextButton.clicked.connect(self.nextActionWrapper)
self.applyNextButton = QPushButton('Apply\nNext')
self.applyNextButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.applyNextButton.clicked.connect(self.applyActionWrapper)
self.applyNextButton.setVisible(False)
self.bBox = QDialogButtonBox()
cancelButton = self.bBox.addButton(QDialogButtonBox.StandardButton.Cancel)
cancelButton.setFocusPolicy(Qt.NoFocus)
applyButton = self.bBox.addButton(QDialogButtonBox.StandardButton.Apply)
applyButton.setFocusPolicy(Qt.NoFocus)
self.bBox.addButton(self.backButton, QDialogButtonBox.ActionRole)
self.bBox.addButton(self.nextButton, QDialogButtonBox.ActionRole)
self.bBox.addButton(self.applyNextButton, QDialogButtonBox.ActionRole)
self.bBox.rejected.connect(self.reject)
self.bBox.accepted.connect(self.accept)
applyButton.clicked.connect(self.accept)
self.display.returnPressed.connect(self.physReturnPressed)
calc_layout = QVBoxLayout()
calc_layout.addLayout(mainLayout)
calc_layout.addWidget(self.bBox)
self.setLayout(calc_layout)
for button in self.bBox.buttons():
if button.text() == 'Cancel' or button.text() == 'Apply':
button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
self.setWindowTitle("Calculator")
if not INFO.LINUXCNC_IS_RUNNING:
self.axisButton.setEnabled(False)
STATUS.connect('all-homed', lambda w: self.axisButton.setEnabled(True))
STATUS.connect('not-all-homed', lambda w, data: self.axisButton.setEnabled(False))
if self.PREFS_:
self.behaviorOnShow = self.PREFS_.getpref('onShowBehavior', 'None', str, self.PREF_SECTION)
else:
self.behaviorOnShow = 'None'
def showEvent(self, event):
if self.behaviorOnShow != 'None':
if 'CLEAR_ALL' in self.behaviorOnShow.upper():
self.clearAll()
if 'FORCE_FOCUS' in self.behaviorOnShow.upper():
self.display.setFocus()
def displayTextEdited(self):
# this only triggers on user changes
if self.display.text() == '':
self.display.setText('0')
def displayTextChanged(self):
# this triggers on both user and programmatic changes
self.display.setStyleSheet("QLineEdit { }")
def physCancelKeyPressed(self):
self.reject()
def physFieldKeyPressed(self, field):
if field == 'next' and self.nextButton.isVisible():
self.nextButton.animateClick()
elif field == 'previous' and self.backButton.isVisible():
self.backButton.animateClick()
def physReturnPressed(self):
if not self.waitingForOperand:
self.equalClicked()
elif self.PREFS_ and self.PREFS_.getpref('acceptOnReturnKey', False, bool, self.PREF_SECTION):
if self.applyNextButton.isVisible():
self.applyNextButton.animateClick()
else:
self.accept()
def physDigitPressed(self, digit):
if self.display.text() == '0' and digit != '0':
self.display.clear()
if self.waitingForOperand:
self.display.clear()
self.waitingForOperand = False
def physOperatorPressed(self, operator):
if operator == '+':
self.plusButton.animateClick()
elif operator == '-':
if self.pendingAdditiveOperator != '-':
self.minusButton.animateClick()
else:
self.changeSignButton.animateClick()
self.pendingAdditiveOperator = ''
self.waitingForOperand = True
self.pendingLabel.clear()
elif operator == '*':
self.timesButton.animateClick()
elif operator == '/':
self.divisionButton.animateClick()
elif operator == '=':
self.equalButton.animateClick()
def digitClicked(self):
clickedButton = self.sender()
digitValue = int(clickedButton.text())
if self.display.text() == '0' and digitValue == 0.0:
return
if self.waitingForOperand:
self.display.clear()
self.waitingForOperand = False
self.display.setText(self.display.text() + str(digitValue))
self.display.setFocus()
def unaryOperatorClicked(self):
clickedButton = self.sender()
clickedOperator = clickedButton.text()
operand = float(self.display.text())
if clickedOperator == "SQRT":
if operand < 0.0:
self.abortOperation()
return
result = math.sqrt(operand)
elif clickedOperator == str("x\N{SUPERSCRIPT TWO}"):
result = math.pow(operand, 2.0)
elif clickedOperator == "1/x":
if operand == 0.0:
self.abortOperation()
return
result = 1.0 / operand
self.display.setText(str(result))
self.display.setFocus()
self.waitingForOperand = True
def additiveOperatorClicked(self):
clickedButton = self.sender()
clickedOperator = clickedButton.text()
operand = float(self.display.text())
if self.pendingMultiplicativeOperator:
if not self.calculate(operand, self.pendingMultiplicativeOperator):
self.abortOperation()
return
self.display.setText(str(self.factorSoFar))
operand = self.factorSoFar
self.factorSoFar = 0.0
self.pendingMultiplicativeOperator = ''
if self.pendingAdditiveOperator and not self.waitingForOperand:
if not self.calculate(operand, self.pendingAdditiveOperator):
self.abortOperation()
return
self.display.setText(str(self.sumSoFar))
else:
self.sumSoFar = operand
self.pendingAdditiveOperator = clickedOperator
self.waitingForOperand = True
self.pendingLabel.setText(clickedOperator)
self.display.setFocus()
def multiplicativeOperatorClicked(self):
clickedButton = self.sender()
clickedOperator = clickedButton.text()
operand = float(self.display.text())
if self.pendingMultiplicativeOperator and not self.waitingForOperand:
if not self.calculate(operand, self.pendingMultiplicativeOperator):
self.abortOperation()
return
self.display.setText(str(self.factorSoFar))
else:
self.factorSoFar = operand
self.pendingMultiplicativeOperator = clickedOperator
self.waitingForOperand = True
self.pendingLabel.setText(clickedOperator)
self.display.setFocus()
def equalClicked(self):
operand = float(self.display.text())
if self.pendingMultiplicativeOperator:
if not self.calculate(operand, self.pendingMultiplicativeOperator):
self.abortOperation()
return
operand = self.factorSoFar
self.factorSoFar = 0.0
self.pendingMultiplicativeOperator = ''
if self.pendingAdditiveOperator:
if not self.calculate(operand, self.pendingAdditiveOperator):
self.abortOperation()
return
self.pendingAdditiveOperator = ''
else:
self.sumSoFar = operand
self.display.setText(str(self.sumSoFar))
self.sumSoFar = 0.0
self.waitingForOperand = True
self.pendingLabel.clear()
self.display.setFocus()
def pointClicked(self):
if self.waitingForOperand:
self.display.setText('0')
if "." not in self.display.text():
self.display.setText(self.display.text() + ".")
self.waitingForOperand = False
self.display.setFocus()
def changeSignClicked(self):
text = self.display.text()
value = float(text)
if value > 0.0:
text = "-" + text
elif value < 0.0:
text = text[1:]
self.display.setText(text)
self.display.setFocus()
def axisClicked(self):
conversion = {'X':0, 'Y':1, "Z":2, 'A':3, "B":4, "C":5, 'U':6, 'V':7, 'W':8}
digitValue = 0.0
try:
p,relp,dtg = STATUS.get_position()
text = self.axisButton.text()
for let in INFO.AVAILABLE_AXES:
if let == text[-1]:
digitValue = round(relp[conversion[let]],5)
text = text.replace('%s'%let,'%s'%digitValue)
break
except Exception as e:
print(e)
return
if self.display.text() == '0' and digitValue == 0.0:
return
if self.waitingForOperand:
self.display.clear()
self.waitingForOperand = False
self.display.setText(str(digitValue))
self.display.setFocus()
def axisTriggered(self, data):
self.axisButton.setText('Axis {}'.format(data))
self.display.setFocus()
def backspaceClicked(self):
if self.waitingForOperand:
return
text = self.display.text()[:-1]
if not text:
text = '0'
self.waitingForOperand = True
self.display.setFocus()
self.display.setText(text)
def convertClicked(self):
clickedOperator = self.sender().property('convert')
operand = float(self.display.text())
if clickedOperator == "to_mm":
result = operand * 25.4
elif clickedOperator == "to_inch":
if operand == 0:
result = 0
else:
result = operand / 25.4
elif clickedOperator == "25.4/X":
if operand == 0:
result = 0
else:
result = 25.4 / operand
else:
return
self.display.setText(str(result))
self.waitingForOperand = True
self.display.setFocus()
def constClicked(self):
clickedButton = self.sender()
constValue = float(clickedButton.text())
if self.waitingForOperand:
self.display.clear()
self.waitingForOperand = False
self.display.setText(str(constValue))
self.display.setFocus()
def clear(self):
if self.waitingForOperand:
return
self.display.setText('0')
self.waitingForOperand = True
self.pendingLabel.clear()
self.display.setFocus()
def clearAll(self):
self.sumSoFar = 0.0
self.factorSoFar = 0.0
self.pendingAdditiveOperator = ''
self.pendingMultiplicativeOperator = ''
self.display.setText('0')
self.display.setFocus()
self.pendingLabel.clear()
self.waitingForOperand = True
def updateMemLabel(self):
self.memoryLabel.setText(f"MEM {self.sumInMemory}")
def clearMemory(self):
self.sumInMemory = 0.0
self.updateMemLabel()
self.display.setFocus()
def readMemory(self):
self.display.setText(str(self.sumInMemory))
self.waitingForOperand = True
self.display.setFocus()
def setMemory(self):
self.equalClicked()
self.sumInMemory = float(self.display.text())
self.updateMemLabel()
self.display.setFocus()
def addToMemory(self):
self.equalClicked()
self.sumInMemory += float(self.display.text())
self.updateMemLabel()
self.display.setFocus()
def createButton(self, text, member, tip=None):
button = QPushButton(text)
button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
button.setFocusPolicy(Qt.NoFocus)
button.setMinimumSize(70, 40)
button.clicked.connect(member)
if tip is not None:
button.setToolTip(tip)
return button
def createAxisButton(self, text, member):
button = QToolButton()
button.setText(text)
button.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
button.setMinimumSize(70, 40)
button.clicked.connect(member)
SettingMenu = QMenu()
button.settingMenu = SettingMenu
for i in INFO.AVAILABLE_AXES:
axisButton = QAction(QIcon('exit24.png'), i, self)
# weird lambda i=i to work around 'function closure'
axisButton.triggered.connect(lambda state, i=i: self.axisTriggered(i))
SettingMenu.addAction(axisButton)
button.setMenu(SettingMenu)
return button
def abortOperation(self):
self.clearAll()
# this should really be dictated by the stylesheet in use
self.display.setStyleSheet("QLineEdit { background-color: #ff7777; }")
self.display.setFocus()
def getDisplay(self):
try:
print(self.display.text())
a = int(self.display.text())
except Exception as e:
self.display.setText('0')
print(e)
print(self.display.text())
return self.display.text()
def calculate(self, rightOperand, pendingOperator):
if pendingOperator == "+":
self.sumSoFar += rightOperand
elif pendingOperator == "-":
self.sumSoFar -= rightOperand
if pendingOperator == str("\N{MULTIPLICATION SIGN}"):
self.factorSoFar *= rightOperand
elif pendingOperator == str("\N{DIVISION SIGN}"):
if rightOperand == 0.0:
return False
self.factorSoFar /= rightOperand
return True
# these wrappers ensure focus is properly managed
def backActionWrapper(self):
self.backAction()
self.display.setFocus()
def nextActionWrapper(self):
self.nextAction()
self.display.setFocus()
def applyActionWrapper(self):
self.applyAction()
self.display.setFocus()
# Subclass can redefine
def backAction(self):
pass
def nextAction(self):
pass
def applyAction(self):
pass
# wait but don't block
# return a display value and ok/cancel
def getValue(self, title=None):
self.setWindowTitle(title)
self.show()
self.applyNextButton.setVisible(False)
self.nextButton.setVisible(False)
self.backButton.setVisible(False)
self._flag = True
while self._flag:
QApplication.processEvents()
return (self.display.text(), self._result)
def accept(self):
super(Calculator, self).accept()
self._result = True
self._flag = False
def reject(self):
super(Calculator, self).reject()
self._result = False
self._flag = False
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
calc = Calculator()
calc.show()
sys.exit(app.exec())