#!/usr/bin/env python3 # qtVcp simple widgets # # Copyright (c) 2017 Chris Morley # # 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 # (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. import enum from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Property, Slot try: from qtpy.QtCore import QEnum except ImportError: try: from qtpy.QtCore import Q_ENUM as QEnum except ImportError: # before Qt 5.10 from qtpy.QtCore import Q_ENUMS as QEnum from qtvcp.widgets.widget_baseclass import (_HalWidgetBase, _HalToggleBase, _HalSensitiveBase, _HalScaleBase) from qtvcp.widgets.indicatorMixIn import IndicatedMixIn from qtvcp.lib.aux_program_loader import Aux_program_loader as _loader from qtvcp.core import Action, Status, Info import hal AUX_PRGM = _loader() ACTION = Action() STATUS = Status() INFO = Info() # Set up logging from qtvcp import logger LOG = logger.getLogger(__name__) # Force the log level for this module #LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL class HALPinType(enum.IntEnum): NONE = 0 BIT = hal.HAL_BIT S32 = hal.HAL_S32 FLOAT = hal.HAL_FLOAT # reacts to HAL pin changes class LCDNumber(QtWidgets.QLCDNumber, _HalWidgetBase): def __init__(self, parent=None): super(LCDNumber, self).__init__(parent) self._floatTemplate = '' self._bit_pin_type = False self._s32_pin_type = False self._float_pin_type = True def _hal_init(self): if self._pin_name_ == '': pname = self.HAL_NAME_ else: pname = self._pin_name_ if self._bit_pin_type: self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_BIT, hal.HAL_IN) self.hal_pin.value_changed.connect(lambda data: self.updateDisplay(data)) elif self._float_pin_type: self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_FLOAT, hal.HAL_IN) self.hal_pin.value_changed.connect(lambda data: self.updateFloatDisplay(data)) elif self._s32_pin_type: self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_S32, hal.HAL_IN) self.hal_pin.value_changed.connect(lambda data: self.updateDisplay(data)) def updateDisplay(self, data): self.display(data) def updateFloatDisplay(self, data): try: if self._floatTemplate == '': self.updateDisplay(data) return t = lambda s: self._floatTemplate % s self.display(t(data)) except: self.display('{:.2f}'.format(data)) LOG.warning("(LCDNumber) Float formatting string: {} not valid-using {}".format( self._floatTemplate, '{:.2f}')) ######################################################################### # This is how designer can interact with our widget properties. # designer will show the Property properties in the editor # it will use the get set and reset calls to do those actions ######################################################################## def _toggle_properties(self, picked): data = ('bit', 's32', 'float') for i in data: if not i == picked: self[i+'_pin_type'] = False def set_pin_name(self, value): self._pin_name_ = value def get_pin_name(self): return self._pin_name_ def reset_pin_name(self): self._pin_name_ = '' def set_bit_pin_type(self, value): self._bit_pin_type = value if value: self._toggle_properties('bit') def get_bit_pin_type(self): return self._bit_pin_type def reset_bit_pin_type(self): self._bit_pin_type = '' def set_s32_pin_type(self, value): self._s32_pin_type = value if value: self._toggle_properties('s32') def get_s32_pin_type(self): return self._s32_pin_type def reset_s32_pin_type(self): self._s32_pin_type = '' def set_float_pin_type(self, value): self._float_pin_type = value if value: self._toggle_properties('float') def get_float_pin_type(self): return self._float_pin_type def reset_float_pin_type(self): self._float_pin_type = '' def set_floatTemplate(self, data): self._floatTemplate = data def get_floatTemplate(self): return self._floatTemplate def reset_floatTemplate(self): self._floatTemplate = '' # designer will show these properties in this order: pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name) bit_pin_type = QtCore.Property(bool, get_bit_pin_type, set_bit_pin_type, reset_bit_pin_type) s32_pin_type = QtCore.Property(bool, get_s32_pin_type, set_s32_pin_type, reset_s32_pin_type) float_pin_type = QtCore.Property(bool, get_float_pin_type, set_float_pin_type, reset_float_pin_type) floatTemplate = QtCore.Property(str, get_floatTemplate, set_floatTemplate, reset_floatTemplate) ############################## # required class boiler code # ############################## def __getitem__(self, item): return getattr(self, item) def __setitem__(self, item, value): return setattr(self, item, value) class CheckBox(QtWidgets.QCheckBox, _HalToggleBase): def __init__(self, parent=None): super(CheckBox, self).__init__(parent) class RadioButton(QtWidgets.QRadioButton, _HalToggleBase): def __init__(self, parent=None): super(RadioButton, self).__init__(parent) class Slider(QtWidgets.QSlider, _HalWidgetBase): def __init__(self, parent=None): super(Slider, self).__init__(parent) def _hal_init(self): if self._pin_name_ == '': pname = self.HAL_NAME_ else: pname = self._pin_name_ self.hal_pin_s = self.HAL_GCOMP_.newpin(str(pname +'-s'), hal.HAL_S32, hal.HAL_OUT) self.hal_pin_f = self.HAL_GCOMP_.newpin(pname +'-f', hal.HAL_FLOAT, hal.HAL_OUT) self.hal_pin_scale = self.HAL_GCOMP_.newpin(pname +'-scale', hal.HAL_FLOAT, hal.HAL_IN) self.hal_pin_scale.set(1) self.updateValue(self.value()) self.valueChanged.connect(lambda data:self.updateValue(data)) def updateValue(self, data): scale = self.hal_pin_scale.get() self.hal_pin_s.set(data) self.hal_pin_f.set(data*scale) def set_pin_name(self, value): self._pin_name_ = value def get_pin_name(self): return self._pin_name_ def reset_pin_name(self): self._pin_name_ = '' pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name) class Dial(QtWidgets.QDial, _HalWidgetBase): def __init__(self, parent=None): super(Dial, self).__init__(parent) self._lastRawCount = 0 self._currentTotalCount = 0 self._deltaScaled = 0 self.epiLow = int(self.maximum() * .25) self.epiHigh = self.maximum() - self.epiLow self.scale = 1 def _hal_init(self): if self._pin_name_ == '': pname = self.HAL_NAME_ else: pname = self._pin_name_ self.hal_pin_s = self.HAL_GCOMP_.newpin(str(pname +'-s'), hal.HAL_S32, hal.HAL_OUT) self.hal_pin_f = self.HAL_GCOMP_.newpin(pname +'-f', hal.HAL_FLOAT, hal.HAL_OUT) self.hal_pin_d = self.HAL_GCOMP_.newpin(pname +'-d', hal.HAL_FLOAT, hal.HAL_OUT) self.hal_pin_scale = self.HAL_GCOMP_.newpin(pname +'-scale', hal.HAL_FLOAT, hal.HAL_IN) self.hal_pin_scale.value_changed.connect(lambda data: self.updateScale(data)) self.hal_pin_scale.set(1) self.updateCount(self.value()) self.valueChanged.connect(lambda data:self.updateCount(data)) def updateScale(self, data): self.scale = data self.hal_pin_f.set(self._currentTotalCount * data) def updateCount(self, count): # wrapping dials -> 0 and maximum is the same position if count == self.maximum() and self.wrapping(): count = 0 delta = self._lastRawCount - count #print 'last:',self._lastRawCount ,'raw count',count,'delta',delta,'count:',count if self._lastRawCount > self.epiHigh and count < self.epiLow : change = self.maximum() - self._lastRawCount + count self._currentTotalCount += change self._deltaScaled += change * self.scale elif count > self.epiHigh and self._lastRawCount < self.epiLow : change = self.maximum() - count + self._lastRawCount self._currentTotalCount -= change self._deltaScaled -= change * self.scale else: self._currentTotalCount -= delta self._deltaScaled -= delta * self.scale self._lastRawCount = count self.hal_pin_s.set(self._currentTotalCount) self.hal_pin_f.set(self._currentTotalCount * self.scale) self.hal_pin_d.set(self._deltaScaled) def set_pin_name(self, value): self._pin_name_ = value def get_pin_name(self): return self._pin_name_ def reset_pin_name(self): self._pin_name_ = '' pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name) class DoubleScale(QtWidgets.QDoubleSpinBox, _HalScaleBase): intOutput = QtCore.Signal(int) floatOutput = QtCore.Signal(float) def __init__(self, parent=None): super(DoubleScale, self).__init__(parent) self.setValue(1) # one can connect signals to this widget to # feed an input that gets scaled by this widget. @QtCore.Slot(float) @QtCore.Slot(int) def setInput(self, data): self.input = data self.valueChanged.emit(self.value()) # call original pin update then # update anything connected to signals def _pin_update(self, data): super(DoubleScale, self)._pin_update(data) self.intOutput.emit(int(self.hal_pin_s.get())) self.floatOutput.emit(self.hal_pin_f.get()) def set_pin_name(self, value): self._pin_name_ = value def get_pin_name(self): return self._pin_name_ def reset_pin_name(self): self._pin_name_ = '' pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name) class GridLayout(QtWidgets.QWidget, _HalSensitiveBase): def __init__(self, parent=None): super(GridLayout, self).__init__(parent) class RichButton(QtWidgets.QPushButton): def __init__(self, parent=None): super(RichButton, self).__init__(parent) self._text = self.text() self._label = QtWidgets.QLabel(self._text, self) self._label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents ) self._label.setAlignment(QtCore.Qt.AlignCenter|QtCore.Qt.AlignVCenter) self._label.show() def setText(self, text): self._label.setText(text) def text(self): try: return self._label.text() except: pass def event(self, event): if event.type() == QtCore.QEvent.Resize: w = QtGui.QResizeEvent.size(event) try: self._label.resize(w.width(), w.height()) self.resize(w.width(), w.height()) except: pass return True else: return super(RichButton, self).event( event) def set_richText(self, data): self.setText(data) def get_richText(self): return self.text() def reset_richText(self): self.setText('Button') richtext_string = QtCore.Property(str, get_richText, set_richText, reset_richText) # button for function callbacks rather then HAL pins class IndicatedPushButton(QtWidgets.QPushButton, IndicatedMixIn): def __init__(self, parent=None): super(IndicatedPushButton, self).__init__(parent) # Override setText function so we can toggle displayed text def setText(self, text): if not self._state_text: super(IndicatedPushButton, self).setText(text) return if self.isCheckable(): if self.isChecked(): super(IndicatedPushButton, self).setText(self._true_string) else: super(IndicatedPushButton, self).setText(self._false_string) elif self._indicator_state: super(IndicatedPushButton, self).setText(self._true_string) else: super(IndicatedPushButton, self).setText(self._false_string) # button controls HAL pins class PushButton(QtWidgets.QPushButton, IndicatedMixIn): HALPinType = HALPinType QEnum(HALPinType) # older version of pyqt5 need this as well as QEnum NONE = 0 BIT = hal.HAL_BIT S32 = hal.HAL_S32 FLOAT = hal.HAL_FLOAT def __init__(self, parent=None): super(PushButton, self).__init__(parent) self._pin_type = HALPinType.S32 self._groupPinName = '' self._exclusiveValue = 0.0 self.halPinGroup = None # Override setText function so we can toggle displayed text def setText(self, text): if not self._state_text: super(PushButton,self).setText(text) return if self.isCheckable(): if self.isChecked(): super(PushButton, self).setText(self._true_string) else: super(PushButton, self).setText(self._false_string) elif self._indicator_state: super(PushButton, self).setText(self._true_string) else: super(PushButton, self).setText(self._false_string) # make the super class (pushbutton) HAL pins # then the button pins # this overrides the super class function # but then calls it after to connect signals etc. def _hal_init(self): if self.autoExclusive(): self.makeGroupPin() if self._pin_name_ == '': pname = self.HAL_NAME_ else: pname = self._pin_name_ self.hal_pin = self.HAL_GCOMP_.newpin(str(pname), hal.HAL_BIT, hal.HAL_OUT) super(PushButton, self)._hal_init() # done so we can adjust 'checkable' signals at run time # ie delete all signals an re apply them # this overrides the super class function def connectSignals(self): super().connectSignals() def _update(state): self.hal_pin.set(state) if self.autoExclusive() and state: self.updateGroup() if self.isCheckable(): self.toggled[bool].connect(_update) else: self.pressed.connect(lambda: _update(True)) self.released.connect(lambda: _update(False)) _update(self.isChecked()) # find the first button in group # it's the 'driver' button widget # then if it's this instance, make a HAL pin for the group def makeGroupPin(self): for i in self.parent().children(): if isinstance(i, PushButton): # are we the group's driver widget? if i is self: # make pin based on type and name if not self._pin_type == HALPinType.NONE: ptype = self._pin_type if self._groupPinName == '': name = HALPinType(self._pin_type).name pname = self.HAL_NAME_ + '.exclusive'+name else: pname = self._groupPinName self.halPinGroup = self.HAL_GCOMP_.newpin(pname , ptype, hal.HAL_OUT) break break # find the driver widget and update it's group HAL pin def updateGroup(self): value = 0 # flag and value of pin for i in self.parent().children(): if isinstance(i, QtWidgets.QAbstractButton): if value == 0: driver = i value = -1 # flag we found the driver if i.isChecked(): #print(self.objectName(),' Checked:',i.objectName() value = i._exclusiveValue #print('value:',value) break if self._pin_type == HALPinType.BIT: data = bool(value) elif self._pin_type == HALPinType.FLOAT: data = float(value) elif self._pin_type == HALPinType.S32: data = int(value) else: return # update the pin of the 'driver' widget try: if not driver.halPinGroup is None: driver.halPinGroup.set(data) except Exception as e: print(e) ######################################################################## # This is how designer can interact with our widget properties. # designer will show the QtCore.Property properties in the editor # it will use the get set and reset calls to do those actions ######################################################################## def set_pin_type(self, value): self._pin_type = value def get_pin_type(self): return self._pin_type def reset_pin_type(self): self._pin_type = HALPinType.S32 # designer will show these properties in this order: pin_type = QtCore.Property(HALPinType, get_pin_type, set_pin_type, reset_pin_type) def set_group_pin_name(self, value): self._groupPinName = value def get_group_pin_name(self): return self._groupPinName def reset_group_pin_name(self): self._groupPinName = '' groupPinName = QtCore.Property(str, get_group_pin_name, set_group_pin_name, reset_group_pin_name) def set_exclusive_value(self, data): self._exclusiveValue = data self.updateGroup() def get_exclusive_value(self): return self._exclusiveValue def reset_exclusive_value(self): self._exclusiveValue = 0.0 self.updateGroup() exclusiveHALValue = QtCore.Property(float, get_exclusive_value, set_exclusive_value, reset_exclusive_value) class ScaledLabel(QtWidgets.QLabel): ''' Label that scales the text based on available size. Base widget for DRO display. ''' def __init__(self, parent=None): super(ScaledLabel, self).__init__(parent) self._text = '' self._scaled = True def _hal_init(self): if self.textFormat() in( 0,1) and self._scaled: self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)) self.setMinSize(15) def textSample(self): ''' Holds a sample of text to reserve space for the longest text. ''' if self._text =='': return self.text() return self._text def setMinSize(self, minfs): f = self.font() f.setPointSizeF(minfs) br = QtGui.QFontMetrics(f).boundingRect(self.textSample()) self.setMinimumSize(br.width(), br.height()) def setMaxSize(self, maxfs): f = self.font() f.setPointSizeF(maxfs) br = QtGui.QFontMetrics(f).boundingRect(self.textSample()) self.setMaximumSize(br.width(), br.height()) def resizeEvent(self, event): super(ScaledLabel, self).resizeEvent(event) if not self._scaled: return #if self.textFormat() == QtCore.Qt.RichText: #print(self.text()) #print(self.styleSheet(),self.text(),self.font().pointSizeF()) if not self.text() or self.textFormat() == QtCore.Qt.AutoText: return #--- fetch current parameters ---- f = self.font() cr = self.contentsRect() #--- iterate to find the font size that fits the contentsRect --- dw = event.size().width() - event.oldSize().width() # width change dh = event.size().height() - event.oldSize().height() # height change fs = max(f.pointSizeF(), .5) while True: f.setPointSize(int(fs)) #gives bigger text #br = QtGui.QFontMetrics(f).tightBoundingRect(self.textSample()) # then this br = QtGui.QFontMetrics(f).boundingRect(self.textSample()) if dw >= 0 and dh >= 0: # label is expanding if br.height() <= cr.height() and br.width() <= cr.width(): fs += .5 else: f.setPointSizeF(max(fs - .5, .5)) # backtrack break else: # label is shrinking if br.height() > cr.height() or br.width() > cr.width(): fs -= .5 else: break if fs < .5: break #print (br, cr) #--- update font size --- if self.textFormat() == QtCore.Qt.RichText: self.setStyleFontSize(f.pointSizeF()) else: self.setFont(f) def setStyleFontSize(self, fs): self.setStyleSheet(' font: {}pt ;'.format(fs)) def set_scaleText(self, data): self._scaled = data def get_scaleText(self): return self._scaled def reset_scaleText(self): self._scaled = True def set_testSample(self, data): self._text = data def get_testSample(self): return self._text def reset_testSample(self): self._text = '' scaleText = QtCore.Property(bool, get_scaleText, set_scaleText, reset_scaleText) textSpaceSample = QtCore.Property(str, get_testSample, set_testSample, reset_testSample)