Import LinuxCNC source tree
3
lib/python/qtvcp/widgets/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Empty package file for widgets provided with Qt Quarterly 26.
|
||||
# Widgets are provided in a package to avoid problems if modules with
|
||||
# generic names are installed in Python's site-packages directory.
|
||||
1827
lib/python/qtvcp/widgets/action_button.py
Normal file
89
lib/python/qtvcp/widgets/action_button_round.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp widget
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
|
||||
# This is an extension of action buttons to show a round button image.
|
||||
# Action buttons are used to control linuxcnc behavior by pressing.
|
||||
# By making the button 'checkable' in the designer editor,
|
||||
# the button will toggle.
|
||||
# In the designer editor, it is possible to select what the button will do.
|
||||
###############################################################################
|
||||
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from qtvcp.widgets.action_button import ActionButton
|
||||
|
||||
class RoundButton(ActionButton):
|
||||
def __init__(self, parent=None):
|
||||
super(RoundButton, self).__init__(parent)
|
||||
self.setCheckable(True)
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
|
||||
self.pixmap = QtGui.QPixmap()
|
||||
self.false_pixmap = QtGui.QPixmap()
|
||||
self.clip_region = QtGui.QRegion(QtCore.QRect(5, 5, self.width()-10, self.height()-10), QtGui.QRegion.Ellipse)
|
||||
|
||||
def action(self, state):
|
||||
super(RoundButton, self).action(state)
|
||||
self.setMask(self.clip_region)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self.clip_region = QtGui.QRegion(QtCore.QRect(5, 5, self.width()-10, self.height()-10), QtGui.QRegion.Ellipse)
|
||||
self.setMask(self.clip_region)
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QtGui.QPainter(self)
|
||||
if self.isChecked():
|
||||
if not self.pixmap.isNull():
|
||||
p.drawPixmap(event.rect(), self.pixmap)
|
||||
return
|
||||
else:
|
||||
if not self.false_pixmap.isNull():
|
||||
p.drawPixmap(event.rect(), self.false_pixmap)
|
||||
return
|
||||
super(RoundButton, self).paintEvent(event)
|
||||
|
||||
#########################################################################
|
||||
# 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 setImagePath(self, data):
|
||||
self.pixmap = data
|
||||
def getImagePath(self):
|
||||
return self.pixmap
|
||||
def resetImagePath(self):
|
||||
self.pixmap
|
||||
|
||||
def setFalseImagePath(self, data):
|
||||
self.false_pixmap = data
|
||||
def getFalseImagePath(self):
|
||||
return self.false_pixmap
|
||||
def resetFalseImagePath(self):
|
||||
self.false_pixmap
|
||||
|
||||
image_path = QtCore.Property(QtGui.QPixmap, getImagePath, setImagePath, resetImagePath)
|
||||
false_image_path = QtCore.Property(QtGui.QPixmap, getFalseImagePath, setFalseImagePath, resetFalseImagePath)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = RoundButton()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
238
lib/python/qtvcp/widgets/action_tool_button.py
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget
|
||||
# Copyright (c) 2018 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import (QToolButton, QMenu, QAction,
|
||||
QComboBox, QWidgetAction)
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.indicatorMixIn import IndicatedMixIn
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class ActionToolButton(QToolButton, IndicatedMixIn):
|
||||
def __init__(self, parent=None):
|
||||
super(ActionToolButton, self).__init__(parent)
|
||||
self._gridSize = 0
|
||||
self._userView = True
|
||||
self._optionMenu = False
|
||||
|
||||
def buildMenu(self):
|
||||
if self._userView:
|
||||
settingMenu = QMenu(self)
|
||||
self.settingMenu = settingMenu
|
||||
self.recordButton = QAction(QIcon('exit24.png'), 'Record View', settingMenu)
|
||||
self.recordButton.triggered.connect(self.recordView)
|
||||
settingMenu.addAction(self.recordButton)
|
||||
self.setMenu(settingMenu)
|
||||
self.clicked.connect(self.setView)
|
||||
|
||||
elif self._optionMenu:
|
||||
settingMenu = QMenu(self)
|
||||
self.settingMenu = settingMenu
|
||||
|
||||
self.showGridAct = QAction( 'Show Grid', settingMenu, checkable=True)
|
||||
self.showGridAct.setChecked(True)
|
||||
self.showGridAct.triggered.connect(self.showGrid)
|
||||
settingMenu.addAction(self.showGridAct)
|
||||
|
||||
self.comboBox = QComboBox(self)
|
||||
|
||||
for i in (INFO.GRID_INCREMENTS):
|
||||
self.comboBox.addItem(i)
|
||||
self.comboBox.currentTextChanged.connect(self.setGridSize)
|
||||
|
||||
wid = QWidgetAction(self)
|
||||
wid.setDefaultWidget(self.comboBox)
|
||||
settingMenu.addAction(wid)
|
||||
|
||||
self.showDimsAct = QAction( 'Show Dimensions', settingMenu, checkable=True)
|
||||
self.showDimsAct.setChecked(True)
|
||||
self.showDimsAct.triggered.connect(self.showDims)
|
||||
settingMenu.addAction(self.showDimsAct)
|
||||
|
||||
self.showDROAct = QAction( 'Show DRO', settingMenu, checkable=True)
|
||||
self.showDROAct.setChecked(False)
|
||||
self.showDROAct.triggered.connect(self.showDRO)
|
||||
settingMenu.addAction(self.showDROAct)
|
||||
|
||||
self.showDTGAct = QAction( 'Show DTG', settingMenu, checkable=True)
|
||||
self.showDTGAct.setChecked(False)
|
||||
self.showDTGAct.triggered.connect(self.showDTG)
|
||||
settingMenu.addAction(self.showDTGAct)
|
||||
|
||||
self.showLargeAct = QAction( 'Large Font', settingMenu, checkable=True)
|
||||
self.showLargeAct.setChecked(False)
|
||||
self.showLargeAct.triggered.connect(self.showLarge)
|
||||
settingMenu.addAction(self.showLargeAct)
|
||||
|
||||
self.setMenu(settingMenu)
|
||||
|
||||
|
||||
# Override setText function so we can toggle displayed text
|
||||
def setText(self, text):
|
||||
if not self._state_text:
|
||||
super(ActionToolButton,self).setText(text)
|
||||
return
|
||||
if self.isCheckable():
|
||||
if self.isChecked():
|
||||
super(ActionToolButton,self).setText(self._true_string)
|
||||
else:
|
||||
super(ActionToolButton,self).setText(self._false_string)
|
||||
elif self._indicator_state:
|
||||
super(ActionToolButton,self).setText(self._true_string)
|
||||
else:
|
||||
super(ActionToolButton,self).setText(self._false_string)
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
super(ActionToolButton, self)._hal_init()
|
||||
self.buildMenu()
|
||||
|
||||
def recordView(self):
|
||||
ACTION.SET_GRAPHICS_VIEW('record-view')
|
||||
|
||||
def setView(self):
|
||||
ACTION.SET_GRAPHICS_VIEW('set-recorded-view')
|
||||
|
||||
def showDims(self, data):
|
||||
if data:
|
||||
ACTION.SET_GRAPHICS_VIEW('dimensions-on')
|
||||
else:
|
||||
ACTION.SET_GRAPHICS_VIEW('dimensions-off')
|
||||
|
||||
def showGrid(self, data):
|
||||
if data:
|
||||
ACTION.SET_GRAPHICS_GRID_SIZE(self._gridSize)
|
||||
else:
|
||||
ACTION.SET_GRAPHICS_VIEW('grid-off')
|
||||
|
||||
def setGridSize(self, data):
|
||||
size = self.parse_increment(data)
|
||||
self._gridSize = size
|
||||
if self.showGridAct.isChecked():
|
||||
ACTION.SET_GRAPHICS_GRID_SIZE(size)
|
||||
|
||||
def showDRO(self, data):
|
||||
if data:
|
||||
ACTION.SET_GRAPHICS_VIEW('overlay-dro-on')
|
||||
else:
|
||||
ACTION.SET_GRAPHICS_VIEW('overlay-dro-off')
|
||||
|
||||
def showDTG(self, data):
|
||||
if data:
|
||||
ACTION.SET_GRAPHICS_VIEW('dtg-on')
|
||||
else:
|
||||
ACTION.SET_GRAPHICS_VIEW('dtg-off')
|
||||
|
||||
def showLarge(self, data):
|
||||
if data:
|
||||
ACTION.SET_GRAPHICS_VIEW('set-large-dro')
|
||||
else:
|
||||
ACTION.SET_GRAPHICS_VIEW('set-small-dro')
|
||||
|
||||
###################################################
|
||||
# helper functions
|
||||
###################################################
|
||||
|
||||
# We convert INI parced increments to machine units
|
||||
def parse_increment(self, gridincr):
|
||||
if gridincr.endswith("mm"):
|
||||
scale = self.conversion(1)
|
||||
elif gridincr.endswith("cm"):
|
||||
scale = self.conversion(10)
|
||||
elif gridincr.endswith("um"):
|
||||
scale = self.conversion(.001)
|
||||
elif gridincr.endswith("in") or gridincr.endswith("inch"):
|
||||
scale = self.conversion(1., metric = False)
|
||||
elif gridincr.endswith("mil"):
|
||||
scale = self.conversion(.001, metric = False)
|
||||
else:
|
||||
scale = 1
|
||||
incr = gridincr.rstrip(" inchmuil")
|
||||
try:
|
||||
if "/" in incr:
|
||||
p, q = incr.split("/")
|
||||
incr = float(p) / float(q)
|
||||
else:
|
||||
incr = float(incr)
|
||||
except:
|
||||
return None
|
||||
LOG.verbose("parsed: text: {} Increment: {} scaled: {}".format(gridincr, incr, (incr * scale)))
|
||||
return incr * scale
|
||||
|
||||
# This does the conversion
|
||||
# calling function must tell us if the data is metric or not.
|
||||
def conversion(self, data, metric = True):
|
||||
if STATUS.is_metric_mode():
|
||||
if metric:
|
||||
return INFO.convert_metric_to_machine(data)
|
||||
else:
|
||||
return INFO.convert_imperial_to_machine(data)
|
||||
else:
|
||||
if metric:
|
||||
return INFO.convert_metric_to_machine(data)
|
||||
else:
|
||||
return INFO.convert_imperial_to_machine(data)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
########################################################################
|
||||
|
||||
# user view
|
||||
def setViewAction(self, state):
|
||||
self._userView = state
|
||||
def getViewAction(self):
|
||||
return self._userView
|
||||
userViewAction = Property(bool, getViewAction, setViewAction)
|
||||
|
||||
# option menu
|
||||
def setOptionMenu(self, state):
|
||||
self._optionMenu = state
|
||||
def getOptionMenu(self):
|
||||
return self._optionMenu
|
||||
OptionMenuAction = Property(bool, getOptionMenu, setOptionMenu)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = ActionToolButton()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
402
lib/python/qtvcp/widgets/adjustment_bar.py
Normal file
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget
|
||||
# Copyright (c) 2018 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import QWidget, QProgressBar, QToolButton, QHBoxLayout, QMenu, QAction
|
||||
from qtpy.QtCore import Qt, Property, QBasicTimer, Signal
|
||||
from qtpy.QtGui import QPainter, QFont, QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp.lib.aux_program_loader import Aux_program_loader
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from LinuxCNC
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds INI details
|
||||
# ACTION gives commands to LinuxCNC
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
AUX_PRGM = Aux_program_loader()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
# So we can customize the label
|
||||
# meaning we can use a new line
|
||||
# to have two levels of text
|
||||
class LabeledBar(QProgressBar):
|
||||
def __init__(self, parent=None):
|
||||
super(LabeledBar, self).__init__(parent)
|
||||
self.text = ''
|
||||
self.setTextVisible(False)
|
||||
self.font = QFont()
|
||||
self.font.setPointSize(8)
|
||||
self.font.setBold(True)
|
||||
self.font.setWeight(75)
|
||||
|
||||
def paintEvent(self, event):
|
||||
super(LabeledBar, self).paintEvent(event)
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
self.drawText(qp)
|
||||
qp.end()
|
||||
|
||||
def drawText(self, qp):
|
||||
qp.setOpacity(1.0)
|
||||
qp.setFont(self.font)
|
||||
qp.drawText(self.rect(), Qt.AlignCenter, self.text)
|
||||
|
||||
|
||||
class HAdjustmentBar(QWidget):
|
||||
valueChanged = Signal(int)
|
||||
def __init__(self, parent=None):
|
||||
super(HAdjustmentBar, self).__init__(parent)
|
||||
self.value = 50
|
||||
self.step = 1
|
||||
self.hi_value = 100
|
||||
self.low_value = 50
|
||||
self.timer_value = 100
|
||||
self.texttemplate = 'Value = %s'
|
||||
self.timer = QBasicTimer()
|
||||
self.showToggleButton = True
|
||||
self.showSettingMenu = True
|
||||
self.bar = LabeledBar()
|
||||
#self.bar.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
|
||||
# black magic class patching
|
||||
# so calling these functions actually calls the self.bar's functions.
|
||||
self.minimum = self.bar.minimum
|
||||
self.maximum = self.bar.maximum
|
||||
|
||||
def buildWidget(self):
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0,0,0,0)
|
||||
SettingMenu = QMenu()
|
||||
exitButton = QAction(QIcon('exit24.png'), 'Set As High', self)
|
||||
exitButton.triggered.connect(self.setHigh)
|
||||
SettingMenu.addAction(exitButton)
|
||||
setlowButton = QAction(QIcon('exit24.png'), 'Set As Low', self)
|
||||
setlowButton.triggered.connect(self.setLow)
|
||||
SettingMenu.addAction(setlowButton)
|
||||
|
||||
self.tb_down = QToolButton()
|
||||
self.tb_down.pressed.connect(self.on_click_down)
|
||||
self.tb_down.released.connect(self.on_released)
|
||||
self.tb_down.setArrowType(Qt.LeftArrow)
|
||||
|
||||
self.tb_up = QToolButton()
|
||||
self.tb_up.pressed.connect(self.on_click_up)
|
||||
self.tb_up.released.connect(self.on_released)
|
||||
self.tb_up.setArrowType(Qt.RightArrow)
|
||||
|
||||
if self.showToggleButton:
|
||||
tb_set = QToolButton()
|
||||
tb_set.clicked.connect(self.on_click_set_value)
|
||||
tb_set.setText('<>')
|
||||
if self.showSettingMenu:
|
||||
tb_set.setMenu(SettingMenu)
|
||||
tb_set.setPopupMode(QToolButton.DelayedPopup)
|
||||
|
||||
layout.addWidget(self.tb_down)
|
||||
layout.addWidget(self.bar)
|
||||
layout.addWidget(self.tb_up)
|
||||
if self.showToggleButton:
|
||||
layout.addWidget(tb_set)
|
||||
layout.setSpacing(0)
|
||||
|
||||
def on_click_up(self):
|
||||
self.timer.start(self.timer_value, self)
|
||||
self.value += self.step
|
||||
self._setValue()
|
||||
|
||||
def on_click_down(self):
|
||||
self.timer.start(self.timer_value, self)
|
||||
self.value -= self.step
|
||||
self._setValue()
|
||||
def on_released(self):
|
||||
self.timer.stop()
|
||||
|
||||
def on_click_set_value(self):
|
||||
if self.value == self.hi_value:
|
||||
self.value = self.low_value
|
||||
else:
|
||||
self.value = self.hi_value
|
||||
self._setValue()
|
||||
|
||||
def timerEvent(self, e):
|
||||
if self.value < self.maximum() and self.value > self.minimum():
|
||||
if self.tb_down.isDown():
|
||||
self.value -= self.step
|
||||
else:
|
||||
self.value += self.step
|
||||
self._setValue()
|
||||
else:
|
||||
self.timer.stop()
|
||||
|
||||
def _setValue(self):
|
||||
if self.value < self.minimum():self. value = self.minimum()
|
||||
if self.value > self.maximum(): self.value = self.maximum()
|
||||
self.valueChanged.emit(self.value)
|
||||
|
||||
def setValue(self, value):
|
||||
if value < self.minimum(): value = self.minimum()
|
||||
if value > self.maximum(): value = self.maximum()
|
||||
self.value = int(value)
|
||||
tmpl = lambda s: self.texttemplate % s
|
||||
try:
|
||||
self.bar.text = tmpl(int(value))
|
||||
except:
|
||||
self.bar.text = self.texttemplate
|
||||
self.bar.setValue(int(value))
|
||||
self.bar.update()
|
||||
|
||||
def setHigh(self):
|
||||
self.hi_value = self.value
|
||||
|
||||
def setLow(self):
|
||||
self.low_value = self.value
|
||||
|
||||
def setMaximum(self, data):
|
||||
if data < self.hi_value:
|
||||
self.hi_value = data
|
||||
self.bar.setMaximum(data)
|
||||
|
||||
def setMinimum(self, data):
|
||||
if data > self.low_value:
|
||||
self.low_value = data
|
||||
self.bar.setMinimum(data)
|
||||
|
||||
def setStep(self, data):
|
||||
self.step = data
|
||||
|
||||
class StatusAdjustmentBar(HAdjustmentBar, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StatusAdjustmentBar, self).__init__(parent)
|
||||
self._block_signal = False
|
||||
self.rapid = True
|
||||
self.feed = False
|
||||
self.spindle = False
|
||||
self.jograte = False
|
||||
self.jograte_angular = False
|
||||
self.maxv = False
|
||||
self.texttemplate = 'Value = %s'
|
||||
|
||||
# self.PREFS_
|
||||
# self.HAL_NAME_
|
||||
# comes from base class
|
||||
def _hal_init(self):
|
||||
|
||||
# if we build the widget before now, designer options are not
|
||||
# taken account
|
||||
self.buildWidget()
|
||||
|
||||
# when estopped disable buttons
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop-reset', lambda w: self.setEnabled(True))
|
||||
|
||||
# set options
|
||||
if self.rapid:
|
||||
STATUS.connect('rapid-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(100)
|
||||
elif self.feed:
|
||||
STATUS.connect('feed-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_FEED_OVERRIDE))
|
||||
elif self.spindle:
|
||||
STATUS.connect('spindle-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_SPINDLE_OVERRIDE))
|
||||
self.setMinimum(int(INFO.MIN_SPINDLE_OVERRIDE))
|
||||
elif self.jograte:
|
||||
STATUS.connect('jograte-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_LINEAR_JOG_VEL))
|
||||
elif self.jograte_angular:
|
||||
STATUS.connect('jograte-angular-changed', lambda w, data: self.setValue(data))
|
||||
print(int(INFO.MAX_ANGULAR_JOG_VEL))
|
||||
self.setMaximum(int(INFO.MAX_ANGULAR_JOG_VEL))
|
||||
elif self.maxv:
|
||||
STATUS.connect('max-velocity-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_TRAJ_VELOCITY))
|
||||
else:
|
||||
LOG.error('{} : no option recognised'.format(self.HAL_NAME_))
|
||||
|
||||
# If there is a preference file object use it to load the hi/low toggle points
|
||||
if self.PREFS_:
|
||||
self.hi_value = self.PREFS_.getpref(self.HAL_NAME_+'-hi-value', 75, int, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.low_value = self.PREFS_.getpref(self.HAL_NAME_+'-low-value', 25, int, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
|
||||
# connect a signal and callback function to the button
|
||||
self.valueChanged.connect(self._action)
|
||||
|
||||
# when qtvcp closes this gets called
|
||||
def _hal_cleanup(self):
|
||||
if self.PREFS_:
|
||||
LOG.debug('Saving {} data to file.'.format(self.HAL_NAME_))
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-hi-value', self.hi_value, int, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-low-value', self.low_value, int, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
|
||||
def _action(self, value):
|
||||
if self.rapid:
|
||||
ACTION.SET_RAPID_RATE(value)
|
||||
elif self.feed:
|
||||
ACTION.SET_FEED_RATE(value)
|
||||
elif self.spindle:
|
||||
ACTION.SET_SPINDLE_RATE(value)
|
||||
elif self.jograte:
|
||||
ACTION.SET_JOG_RATE(value)
|
||||
elif self.jograte_angular:
|
||||
ACTION.SET_JOG_RATE_ANGULAR(value)
|
||||
elif self.maxv:
|
||||
ACTION.SET_MAX_VELOCITY_RATE(value)
|
||||
else:
|
||||
LOG.error('{} no action recognised'.format(self.HAL_NAME_))
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('rapid', 'feed', 'spindle', 'jograte', 'jograte_angular', 'maxv')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i+'_rate'] = False
|
||||
|
||||
def setrapid(self, data):
|
||||
self.rapid = data
|
||||
if data:
|
||||
self._toggle_properties('rapid')
|
||||
def getrapid(self):
|
||||
return self.rapid
|
||||
def resetrapid(self):
|
||||
self.rapid = False
|
||||
|
||||
def setfeed(self, data):
|
||||
self.feed = data
|
||||
if data:
|
||||
self._toggle_properties('feed')
|
||||
def getfeed(self):
|
||||
return self.feed
|
||||
def resetfeed(self):
|
||||
self.feed = False
|
||||
|
||||
def setspindle(self, data):
|
||||
self.spindle = data
|
||||
if data:
|
||||
self._toggle_properties('spindle')
|
||||
def getspindle(self):
|
||||
return self.spindle
|
||||
def resetspindle(self):
|
||||
self.spindle = False
|
||||
|
||||
def setjograte(self, data):
|
||||
self.jograte = data
|
||||
if data:
|
||||
self._toggle_properties('jograte')
|
||||
def getjograte(self):
|
||||
return self.jograte
|
||||
def resetjograte(self):
|
||||
self.jograte = False
|
||||
|
||||
def setjograteangular(self, data):
|
||||
self.jograte_angular = data
|
||||
if data:
|
||||
self._toggle_properties('jograte_angular')
|
||||
def getjograteangular(self):
|
||||
return self.jograte_angular
|
||||
def resetjograteangular(self):
|
||||
self.jograte_angular = False
|
||||
|
||||
def setmaxv(self, data):
|
||||
self.maxv = data
|
||||
if data:
|
||||
self._toggle_properties('maxv')
|
||||
def getmaxv(self):
|
||||
return self.maxv
|
||||
def resetmaxv(self):
|
||||
self.maxv = False
|
||||
|
||||
def setshowtoggle(self, data):
|
||||
self.showToggleButton = data
|
||||
def getshowtoggle(self):
|
||||
return self.showToggleButton
|
||||
def resetshowtoggle(self):
|
||||
self.showToggleButton = True
|
||||
|
||||
def setsettingmenu(self, data):
|
||||
self.showSettingMenu = data
|
||||
def getsettingmenu(self):
|
||||
return self.showSettingMenu
|
||||
def resetsettingmenu(self):
|
||||
self.showSettingMenu = True
|
||||
|
||||
# Designer makes '\n' into '\\n' if added
|
||||
# we put fix it here so we actually get the newline
|
||||
def settexttemplate(self, data):
|
||||
data = data.replace('\\n' , '\n')
|
||||
self.texttemplate = data
|
||||
def gettexttemplate(self):
|
||||
return self.texttemplate
|
||||
def resettexttemplate(self):
|
||||
self.texttemplate = 'Value = %s'
|
||||
|
||||
def setsteprate(self, data):
|
||||
self.step = data
|
||||
def getsteprate(self):
|
||||
return self.step
|
||||
def resetsteprate(self):
|
||||
self.steprate = 1
|
||||
|
||||
rapid_rate = Property(bool, getrapid, setrapid, resetrapid)
|
||||
feed_rate = Property(bool, getfeed, setfeed, resetfeed)
|
||||
spindle_rate = Property(bool, getspindle, setspindle, resetspindle)
|
||||
jograte_rate = Property(bool, getjograte, setjograte, resetjograte)
|
||||
jograte_angular_rate = Property(bool, getjograteangular, setjograteangular, resetjograteangular)
|
||||
max_velocity_rate = Property(bool, getmaxv, setmaxv, resetmaxv)
|
||||
show_toggle_button = Property(bool, getshowtoggle, setshowtoggle, resetshowtoggle)
|
||||
show_setting_menu = Property(bool, getsettingmenu, setsettingmenu, resetsettingmenu)
|
||||
|
||||
text_template = Property(str, gettexttemplate, settexttemplate, resettexttemplate)
|
||||
step_rate = Property(int, getsteprate, setsteprate, resetsteprate)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = StatusAdjustmentBar()
|
||||
widget.PREFS_=None
|
||||
widget.buildWidget()
|
||||
widget.settexttemplate('Text %s')
|
||||
widget.setValue(75)
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
90
lib/python/qtvcp/widgets/axisPickTool_button.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from qtpy.QtWidgets import (QToolButton, QMenu, QAction,
|
||||
QComboBox, QWidgetAction, QSizePolicy)
|
||||
from qtpy.QtCore import Property, Signal
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.indicatorMixIn import IndicatedMixIn
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class AxisPickToolButton(QToolButton, IndicatedMixIn):
|
||||
AxisSelected = Signal(str)
|
||||
CurrentAxisPosition = Signal(str,float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(AxisPickToolButton, self).__init__(parent)
|
||||
self._textTemplate = 'Axis: %s'
|
||||
self._currentAxis = 'X'
|
||||
|
||||
|
||||
def _hal_init(self):
|
||||
self.createAxisButton(self._currentAxis)
|
||||
self.axisTriggered(self._currentAxis)
|
||||
|
||||
def createAxisButton(self, text):
|
||||
tmpl = lambda s: self._textTemplate % s
|
||||
self.setText(tmpl(text))
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
self.setMinimumSize(70, 40)
|
||||
self.clicked.connect(self.axisClicked)
|
||||
SettingMenu = QMenu()
|
||||
self.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)
|
||||
self.setMenu(SettingMenu)
|
||||
|
||||
def axisTriggered(self, data):
|
||||
tmpl = lambda s: self._textTemplate % s
|
||||
self.setText(tmpl(data))
|
||||
self.AxisSelected.emit(data)
|
||||
|
||||
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.text()
|
||||
for let in INFO.AVAILABLE_AXES:
|
||||
if let == text[-1]:
|
||||
digitValue = round(p[conversion[let]],5)
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
print(let, digitValue)
|
||||
self.AxisSelected.emit(let)
|
||||
self.CurrentAxisPosition.emit(let, digitValue)
|
||||
|
||||
def setTextTemplate(self, data):
|
||||
self._textTemplate = data
|
||||
def getTextTemplate(self):
|
||||
return self._textTemplate
|
||||
def resetTextTemplate(self):
|
||||
self._textTemplate = 'Axis: %s'
|
||||
textTemplate = Property(str, getTextTemplate, setTextTemplate, resetTextTemplate)
|
||||
|
||||
def setCurrentAxis(self, data):
|
||||
self._currentAxis = data
|
||||
def getCurrentAxis(self):
|
||||
return self._currentAxis
|
||||
def resetCurrentAxis(self):
|
||||
self._currentAxis = 'X'
|
||||
currentAxis = Property(str, getCurrentAxis, setCurrentAxis, resetCurrentAxis)
|
||||
|
||||
404
lib/python/qtvcp/widgets/axis_tool_button.py
Normal file
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget
|
||||
# Copyright (c) 2018 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 hal
|
||||
|
||||
from qtpy.QtWidgets import QToolButton, QMenu, QAction
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.indicatorMixIn import IndicatedMixIn
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class AxisToolButton(QToolButton, IndicatedMixIn):
|
||||
def __init__(self, parent=None):
|
||||
super(AxisToolButton, self).__init__(parent)
|
||||
self._joint = 0
|
||||
self._axis = ''
|
||||
self._block_signal = False
|
||||
self._halpin_option = True
|
||||
self.dialog_code = 'ENTRY'
|
||||
self.display_units_mm = 0
|
||||
homeOption = False
|
||||
|
||||
self._showGoto = False
|
||||
self._showZero = True
|
||||
self._showSet = True
|
||||
self._showLast = True
|
||||
self._showDivide = True
|
||||
|
||||
SettingMenu = QMenu(self)
|
||||
self.settingMenu = SettingMenu
|
||||
self.zeroButton = QAction(QIcon('exit24.png'), 'Zero', self)
|
||||
self.zeroButton.triggered.connect(self.Zero)
|
||||
SettingMenu.addAction(self.zeroButton)
|
||||
self.setButton = QAction(QIcon('exit24.png'), 'Set', self)
|
||||
self.setButton.triggered.connect(self.SetOrigin)
|
||||
SettingMenu.addAction(self.setButton)
|
||||
self.divideButton = QAction(QIcon('exit24.png'), 'Divide By 2', self)
|
||||
self.divideButton.triggered.connect(self.Divide)
|
||||
SettingMenu.addAction(self.divideButton)
|
||||
self.lastButton = QAction(QIcon('exit24.png'), 'Set To Last', self)
|
||||
self.lastButton.triggered.connect(self.Last)
|
||||
SettingMenu.addAction(self.lastButton)
|
||||
self.goToG53Button = QAction(self)
|
||||
self.goToG53Button.triggered.connect(self.goToG53)
|
||||
SettingMenu.addAction(self.goToG53Button)
|
||||
self.goToG5xButton = QAction(self)
|
||||
self.goToG5xButton.triggered.connect(self.goToG5x)
|
||||
SettingMenu.addAction(self.goToG5xButton)
|
||||
|
||||
# option for later - should put a sub menu in for joints homing though
|
||||
if homeOption:
|
||||
SettingMenu.addSeparator()
|
||||
self.homeButton = QAction(QIcon('exit24.png'), 'Home', self)
|
||||
self.homeButton.triggered.connect(self.Home)
|
||||
SettingMenu.addAction(self.homeButton)
|
||||
#self.homeButton.setEnabled(False)
|
||||
|
||||
self.UnHomeButton = QAction(QIcon('exit24.png'), 'Unhome', self)
|
||||
self.UnHomeButton.triggered.connect(self.Unhome)
|
||||
SettingMenu.addAction(self.UnHomeButton)
|
||||
#self.homeButton.setEnabled(False)
|
||||
|
||||
self.setMenu(SettingMenu)
|
||||
self.clicked.connect(self.selectJoint)
|
||||
self.toggled.connect(self.selectJoint)
|
||||
|
||||
# Override setText function so we can toggle displayed text
|
||||
def setText(self, text):
|
||||
if not self._state_text:
|
||||
super(AxisToolButton,self).setText(text)
|
||||
return
|
||||
if self.isCheckable():
|
||||
if self.isChecked():
|
||||
super(AxisToolButton,self).setText(self._true_string)
|
||||
else:
|
||||
super(AxisToolButton,self).setText(self._false_string)
|
||||
elif self._indicator_state:
|
||||
super(AxisToolButton,self).setText(self._true_string)
|
||||
else:
|
||||
super(AxisToolButton,self).setText(self._false_string)
|
||||
|
||||
def _hal_init(self):
|
||||
super(AxisToolButton, self)._hal_init()
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('state-off', lambda w: self._enableGroup(True,False))
|
||||
STATUS.connect('state-on', lambda w: self._enableGroup(True, True))
|
||||
STATUS.connect('interp-idle', lambda w: self._enableGroup(homed_on_test(),STATUS.machine_is_on()))
|
||||
STATUS.connect('interp-run', lambda w: self._enableGroup(False, False))
|
||||
STATUS.connect('all-homed', lambda w: self._enableGroup(homed_on_test(), STATUS.machine_is_on()))
|
||||
STATUS.connect('not-all-homed', lambda w, data: self._enableGroup(False,STATUS.machine_is_on()))
|
||||
STATUS.connect('interp-paused', lambda w: self._enableGroup(False,False))
|
||||
STATUS.connect('motion-mode-changed', lambda w,data: self.modeChanged(data))
|
||||
STATUS.connect('joint-selection-changed', lambda w,data: self.ChangeState(joint = data, axis= STATUS.get_selected_axis()))
|
||||
STATUS.connect('axis-selection-changed', lambda w,data: self.ChangeState(joint = STATUS.get_selected_joint(), axis = data))
|
||||
if self._halpin_option and self._joint != -1:
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
|
||||
self.hal_pin_joint = self.HAL_GCOMP_.newpin(str(pname + '-joint'), hal.HAL_BIT, hal.HAL_OUT)
|
||||
self.hal_pin_axis = self.HAL_GCOMP_.newpin(str(pname + '-axis'), hal.HAL_BIT, hal.HAL_OUT)
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
def _enableGroup(self, state,bstate):
|
||||
for i in(self.zeroButton, self.setButton,self.divideButton,
|
||||
self.lastButton,self.goToG53Button, self.goToG5xButton ):
|
||||
i.setEnabled(state)
|
||||
|
||||
self.setEnabled(bstate)
|
||||
|
||||
def Zero(self):
|
||||
axis, now = self._a_from_j(self._axis)
|
||||
if axis:
|
||||
ACTION.SET_AXIS_ORIGIN(axis, 0)
|
||||
STATUS.emit('update-machine-log', 'Zeroed Axis %s' % axis, 'TIME,SUCCESS')
|
||||
|
||||
def SetOrigin(self):
|
||||
axis, now = self._a_from_j(self._axis)
|
||||
if axis:
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,'CURRENT':now,
|
||||
'TITLE':'Set %s Origin'% axis,
|
||||
'GEONAME':'axisToolButtonDialog_{}'.format(self.dialog_code),
|
||||
'AXIS':axis}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
axis = message.get('AXIS')
|
||||
ACTION.SET_AXIS_ORIGIN(axis, num)
|
||||
STATUS.emit('update-machine-log', 'Set Origin of Axis %s to %f' %(axis, num), 'TIME,SUCCESS')
|
||||
|
||||
def Divide(self):
|
||||
axis, now = self._a_from_j(self._axis)
|
||||
if axis:
|
||||
try:
|
||||
x = now/2.0
|
||||
ACTION.SET_AXIS_ORIGIN(axis, x)
|
||||
text = 'Divided Axis %s in Half - %f'% (axis, x)
|
||||
STATUS.emit('update-machine-log', text, 'TIME,SUCCESS')
|
||||
except ZeroDivisionError:
|
||||
pass
|
||||
|
||||
def Last(self):
|
||||
axis, now = self._a_from_j(self._axis)
|
||||
if axis:
|
||||
last = ACTION.GET_LAST_RECORDED_ORIGIN(axis)
|
||||
ACTION.SET_AXIS_ORIGIN(axis, last)
|
||||
text = 'Reset Axis %s from %f to Last Value: %f' %(axis, now, last)
|
||||
STATUS.emit('update-machine-log', text, 'TIME,SUCCESS')
|
||||
|
||||
def Home(self):
|
||||
#axis, now = self._a_from_j(self._axis)
|
||||
#if axis:
|
||||
ACTION.SET_MACHINE_HOMING(self._joint)
|
||||
STATUS.emit('update-machine-log', 'Homed Axis %s' % self._joint, 'TIME,SUCCESS')
|
||||
|
||||
def Unhome(self):
|
||||
#axis, now = self._a_from_j(self._axis)
|
||||
#if axis:
|
||||
ACTION.SET_MACHINE_UNHOMED(self._joint)
|
||||
STATUS.emit('update-machine-log', 'Unhomed Axis %s' % self._joint, 'TIME,WARNING')
|
||||
|
||||
def goToG53(self):
|
||||
ACTION.CALL_MDI('G53 G0 {}0'.format(self._axis))
|
||||
|
||||
def goToG5x(self):
|
||||
ACTION.CALL_MDI('G0 {}0'.format(self._axis))
|
||||
|
||||
# if in joint mode -nope
|
||||
# if axis is '' the find the selected axis from gstat
|
||||
# find the joint number so we can get current position
|
||||
def _a_from_j(self, axis):
|
||||
if STATUS.is_joint_mode():
|
||||
return None, None
|
||||
if axis == '':
|
||||
axis = STATUS.get_selected_axis()
|
||||
j = "XYZABCUVW"
|
||||
try:
|
||||
jnum = j.find(axis)
|
||||
except:
|
||||
return None, None
|
||||
p,r,d = STATUS.get_position()
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
r = INFO.convert_units_9(r)
|
||||
return axis, r[jnum]
|
||||
|
||||
def selectJoint(self):
|
||||
if self._block_signal or self._joint == -1 or self._axis == '': return
|
||||
if self.isChecked() == True:
|
||||
if STATUS.is_joint_mode():
|
||||
ACTION.SET_SELECTED_JOINT(self._joint)
|
||||
if self._halpin_option:
|
||||
self.hal_pin_joint.set(self.isChecked())
|
||||
else:
|
||||
ACTION.SET_SELECTED_AXIS(self._axis)
|
||||
# set this without causing a STATUS message output
|
||||
# in case we are selecting an axis to un/home
|
||||
STATUS.selected_joint = self._joint
|
||||
if self._halpin_option:
|
||||
self.hal_pin_axis.set(self.isChecked())
|
||||
else:
|
||||
if STATUS.is_joint_mode():
|
||||
ACTION.SET_SELECTED_JOINT(-1)
|
||||
else:
|
||||
ACTION.SET_SELECTED_AXIS('None')
|
||||
# set this without causing a STATUS message output
|
||||
# in case we are selecting an axis to un/home
|
||||
STATUS.selected_joint = -1
|
||||
if self._halpin_option:
|
||||
self.hal_pin_axis.set(False)
|
||||
|
||||
def ChangeState(self, joint = None, axis = None):
|
||||
#print(self.objectName(),'change',joint,axis,self._axis)
|
||||
# joint mode
|
||||
if STATUS.is_joint_mode():
|
||||
if int(joint) != self._joint:
|
||||
self._block_signal = True
|
||||
self.setChecked(False)
|
||||
self._block_signal = False
|
||||
if self._halpin_option and self._joint != -1:
|
||||
self.hal_pin_joint.set(False)
|
||||
else:
|
||||
self._block_signal = True
|
||||
self.setChecked(True)
|
||||
self._block_signal = False
|
||||
if self._halpin_option and self._axis != '':
|
||||
self.hal_pin_joint.set(True)
|
||||
# axis mode
|
||||
else:
|
||||
if str(axis) != self._axis and self.isChecked():
|
||||
self._block_signal = True
|
||||
self.setChecked(False)
|
||||
self._block_signal = False
|
||||
if self._halpin_option and self._axis != '':
|
||||
self.hal_pin_joint.set(False)
|
||||
elif str(axis) == self._axis and not self.isChecked():
|
||||
self._block_signal = True
|
||||
self.setChecked(True)
|
||||
self._block_signal = False
|
||||
if self._halpin_option and self._axis != '':
|
||||
self.hal_pin_joint.set(True)
|
||||
|
||||
def modeChanged(self, mode):
|
||||
self.selectJoint()
|
||||
|
||||
def _switch_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
|
||||
#########################################################################
|
||||
# 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 set_joint(self, data):
|
||||
self._joint = data
|
||||
if data == -1:
|
||||
self.setPopupMode(QToolButton.InstantPopup)
|
||||
def get_joint(self):
|
||||
return self._joint
|
||||
def reset_joint(self):
|
||||
self._joint = -1
|
||||
joint_number = Property(int, get_joint, set_joint, reset_joint)
|
||||
|
||||
def set_axis(self, data):
|
||||
if data.upper() in('X','Y','Z','A','B','C','U','V','W'):
|
||||
self._axis = str(data.upper())
|
||||
text = 'Go To G53 Origin in {}'.format(self._axis)
|
||||
self.goToG53Button.setText(text)
|
||||
text = 'Go To G5x Origin in {}'.format(self._axis)
|
||||
self.goToG5xButton.setText(text)
|
||||
else:
|
||||
self._axis = str('')
|
||||
|
||||
def get_axis(self):
|
||||
return self._axis
|
||||
def reset_axis(self):
|
||||
self._axis = 'X'
|
||||
axis_letter = Property(str, get_axis, set_axis, reset_axis)
|
||||
|
||||
def set_halpin_option(self, value):
|
||||
self._halpin_option = value
|
||||
def get_halpin_option(self):
|
||||
return self._halpin_option
|
||||
def reset_halpin_option(self):
|
||||
self._halpin_option = True
|
||||
halpin_option = Property(bool, get_halpin_option, set_halpin_option, reset_halpin_option)
|
||||
|
||||
def set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'ENTRY'
|
||||
dialog_code_string = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
####################
|
||||
## menu properties
|
||||
####################
|
||||
|
||||
def set_showSet(self, data):
|
||||
self._showSet = data
|
||||
self.setButton.setVisible(data)
|
||||
def get_showSet(self):
|
||||
return self._showSet
|
||||
def reset_showSet(self):
|
||||
self._showSet = True
|
||||
showSetOrigin = Property(bool, get_showSet, set_showSet, reset_showSet)
|
||||
|
||||
def set_showZero(self, data):
|
||||
self._showZero = data
|
||||
self.zeroButton.setVisible(data)
|
||||
def get_showZero(self):
|
||||
return self._showZero
|
||||
def reset_showZero(self):
|
||||
self._showZero = True
|
||||
showZeroOrigin = Property(bool, get_showZero, set_showZero, reset_showZero)
|
||||
|
||||
def set_showGoto(self, data):
|
||||
self._showGoto = data
|
||||
self.goToG53Button.setVisible(data)
|
||||
self.goToG5xButton.setVisible(data)
|
||||
def get_showGoto(self):
|
||||
return self._showGoto
|
||||
def reset_showGoto(self):
|
||||
self._showGoto = False
|
||||
showGotoOrigin = Property(bool, get_showGoto, set_showGoto, reset_showGoto)
|
||||
|
||||
def set_showLast(self, data):
|
||||
self._showLast = data
|
||||
self.lastButton.setVisible(data)
|
||||
def get_showLast(self):
|
||||
return self._showLast
|
||||
def reset_showLast(self):
|
||||
self._showLast = True
|
||||
showLast = Property(bool, get_showLast, set_showLast, reset_showLast)
|
||||
|
||||
def set_showDivide(self, data):
|
||||
self._showDivide = data
|
||||
self.divideButton.setVisible(data)
|
||||
def get_showDivide(self):
|
||||
return self._showDivide
|
||||
def reset_showDivide(self):
|
||||
self._showDivide = True
|
||||
showDivide = Property(bool, get_showDivide, set_showDivide, reset_showDivide)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = AxisToolButton()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
544
lib/python/qtvcp/widgets/bar.py
Normal file
@@ -0,0 +1,544 @@
|
||||
import enum
|
||||
from qtpy import QtWidgets
|
||||
from qtpy.QtGui import QColor, QBrush, QPainter, QLinearGradient
|
||||
from qtpy.QtCore import (Qt, Slot, Property, Signal, QRectF,
|
||||
QSize)
|
||||
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
|
||||
import hal
|
||||
|
||||
# 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 Bar(QtWidgets.QWidget):
|
||||
valueChanged = Signal([int],[float])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._vertical = False
|
||||
self._opposite = False
|
||||
self._minimum = 0
|
||||
self._maximum = 200
|
||||
self._value = 100
|
||||
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding
|
||||
)
|
||||
|
||||
# set default number of colored steps
|
||||
self.setSteps(None)
|
||||
|
||||
self._bar_solid_percent = 0.9
|
||||
self._background_color = QColor('black')
|
||||
self._padding = 2.0 # n-pixel gap around edge.
|
||||
self._split = .2 # percent of full scale indicator
|
||||
self._useMultiColor = True
|
||||
self._singleIndicatorColor = QColor('blue')
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QPainter(self)
|
||||
|
||||
brush = QBrush()
|
||||
brush.setColor(self._background_color)
|
||||
brush.setStyle(Qt.SolidPattern)
|
||||
rect = QRectF(0, 0, painter.device().width(), painter.device().height())
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# Get current state.
|
||||
vmin, vmax = self.minimum(), self.maximum()
|
||||
value = self.value()
|
||||
|
||||
# Define our canvas.
|
||||
d_height = painter.device().height() - (self._padding * 2)
|
||||
d_width = painter.device().width() - (self._padding * 2)
|
||||
|
||||
if self._vertical:
|
||||
# Draw the vertical bars.
|
||||
step_size = d_height / self.n_steps
|
||||
bar_height = step_size * self._bar_solid_percent
|
||||
bar_spacer = step_size * (1 - self._bar_solid_percent) / 2
|
||||
|
||||
# Calculate the y-stop position, from the value in range.
|
||||
pc = (value - vmin) / (vmax - vmin)
|
||||
n_steps_to_draw = int(pc * self.n_steps)
|
||||
partial = (pc * self.n_steps) % 1
|
||||
#print('pre',value,pc, n_steps_to_draw, partial)
|
||||
|
||||
n =-1 # default fall through value if n_steps_to_draw = 0
|
||||
# draw complete step bars
|
||||
for n in range(self.n_steps):
|
||||
brush.setColor(QColor(self._step_color_list[n]))
|
||||
if self._opposite:
|
||||
top = self._padding + (( + n) * step_size) + bar_spacer
|
||||
else:
|
||||
top = self._padding + d_height - ((1 + n) * step_size) + bar_spacer
|
||||
|
||||
# full scale indicator:
|
||||
# QRextF(left, top, width and height )
|
||||
rect = QRectF(
|
||||
self._padding,
|
||||
top,
|
||||
d_width * self._split,
|
||||
bar_height
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# value indicator:
|
||||
if n < n_steps_to_draw:
|
||||
rect2 = QRectF(
|
||||
self._padding + (d_width * self._split),
|
||||
top ,
|
||||
d_width * (1-self._split),
|
||||
bar_height
|
||||
)
|
||||
if self._useMultiColor:
|
||||
color = QColor(self._step_color_list[n])
|
||||
else:
|
||||
color = self._singleIndicatorColor
|
||||
gradient = QLinearGradient(0, 0, 1, 0)
|
||||
gradient.setColorAt(0.0, color)
|
||||
gradient.setColorAt(0.5, color.lighter())
|
||||
gradient.setColorAt(1.0, color)
|
||||
gradient.setCoordinateMode(gradient.ObjectMode)
|
||||
painter.fillRect(rect2, QBrush(gradient))
|
||||
|
||||
# draw partial step bar for in-between values
|
||||
if partial > 0:
|
||||
n = n_steps_to_draw
|
||||
brush.setColor(QColor(self._step_color_list[n]))
|
||||
# right to left
|
||||
height = (bar_height* partial)
|
||||
if self._opposite:
|
||||
top = self._padding + (n * step_size) + bar_spacer
|
||||
else:
|
||||
top = self._padding - (n * step_size) + d_height - height- bar_spacer
|
||||
|
||||
# full scale indicator
|
||||
rect = QRectF(
|
||||
self._padding,
|
||||
top,
|
||||
d_width * self._split,
|
||||
height
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# value indicator
|
||||
rect2 = QRectF(
|
||||
self._padding + d_width * self._split,
|
||||
top ,
|
||||
d_width *(1- self._split),
|
||||
height
|
||||
)
|
||||
|
||||
if self._useMultiColor:
|
||||
color = QColor(self._step_color_list[n])
|
||||
else:
|
||||
color = self._singleIndicatorColor
|
||||
|
||||
gradient = QLinearGradient(0, 0, 1, 0)
|
||||
gradient.setColorAt(0.0, color)
|
||||
gradient.setColorAt(0.5, color.lighter())
|
||||
gradient.setColorAt(1.0, color)
|
||||
gradient.setCoordinateMode(gradient.ObjectMode)
|
||||
painter.fillRect(rect2, QBrush(gradient))
|
||||
|
||||
|
||||
else:
|
||||
# Draw the horizontal bars.
|
||||
step_size = d_width / self.n_steps
|
||||
bar_width = step_size * self._bar_solid_percent
|
||||
bar_spacer = step_size * (1 - self._bar_solid_percent) / 2
|
||||
|
||||
# Calculate the y-stop position, from the value in range.
|
||||
pc = (value - vmin) / (vmax - vmin)
|
||||
n_steps_to_draw = int(pc * self.n_steps)
|
||||
partial = (pc * self.n_steps) % 1
|
||||
#print('pre',value,pc, n_steps_to_draw, partial)
|
||||
|
||||
n=-1 # default fall through value if n_steps_to_draw = 0
|
||||
for n in range(self.n_steps):
|
||||
brush.setColor(QColor(self._step_color_list[n]))
|
||||
# left, top, width and height floats
|
||||
if self._opposite:
|
||||
left = self._padding + d_width - ((1 + n) * step_size) + bar_spacer
|
||||
else:
|
||||
left = self._padding + bar_spacer + ((0 + n) * step_size)
|
||||
rect = QRectF(
|
||||
left ,
|
||||
self._padding ,
|
||||
bar_width,
|
||||
d_height * self._split
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
if n < n_steps_to_draw:
|
||||
rect2 = QRectF(
|
||||
left ,
|
||||
self._padding + d_height * self._split,
|
||||
bar_width,
|
||||
d_height * (1-self._split)
|
||||
)
|
||||
|
||||
if self._useMultiColor:
|
||||
color = QColor(self._step_color_list[n])
|
||||
else:
|
||||
color = self._singleIndicatorColor
|
||||
|
||||
gradient = QLinearGradient(0, 1, 0, 0)
|
||||
gradient.setColorAt(0.0, color)
|
||||
gradient.setColorAt(0.5, color.lighter())
|
||||
gradient.setColorAt(1.0, color)
|
||||
gradient.setCoordinateMode(gradient.ObjectMode)
|
||||
painter.fillRect(rect2, QBrush(gradient))
|
||||
|
||||
# draw partial step bar for in-between values
|
||||
if partial > 0:
|
||||
n = n_steps_to_draw
|
||||
brush.setColor(QColor(self._step_color_list[n]))
|
||||
|
||||
# right to left
|
||||
width = (bar_width* partial)
|
||||
if self._opposite:
|
||||
left = self._padding - (n * step_size) + d_width - width - bar_spacer
|
||||
# left to right
|
||||
else:
|
||||
left = self._padding + bar_spacer + (n * step_size)
|
||||
|
||||
# full scale indicator
|
||||
rect = QRectF(
|
||||
left,
|
||||
self._padding ,
|
||||
width,
|
||||
d_height * self._split
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# value indicator
|
||||
rect2 = QRectF(
|
||||
left ,
|
||||
self._padding + d_height * self._split,
|
||||
width,
|
||||
d_height * (1-self._split)
|
||||
)
|
||||
if self._useMultiColor:
|
||||
color = QColor(self._step_color_list[n])
|
||||
else:
|
||||
color = self._singleIndicatorColor
|
||||
gradient = QLinearGradient(0, 1, 0, 0)
|
||||
gradient.setColorAt(0.0, color)
|
||||
gradient.setColorAt(0.5, color.lighter())
|
||||
gradient.setColorAt(1.0, color)
|
||||
gradient.setCoordinateMode(gradient.ObjectMode)
|
||||
painter.fillRect(rect2, QBrush(gradient))
|
||||
|
||||
painter.end()
|
||||
|
||||
# list of color strings or an integer of steps
|
||||
def setSteps(self, steps):
|
||||
if steps is None:
|
||||
steps = ['green', 'green', '#00b600', '#00b600', '#00d600',
|
||||
'#00d600', 'yellow', 'yellow', 'red', 'red']
|
||||
self.n_steps = len(steps)
|
||||
self._step_color_list = steps
|
||||
|
||||
if isinstance(steps, list):
|
||||
# list of colors.
|
||||
self.n_steps = len(steps)
|
||||
self._step_color_list = steps
|
||||
|
||||
elif isinstance(steps, int):
|
||||
# int number of bars, defaults to red.
|
||||
self.n_steps = steps
|
||||
self._step_color_list = ['red'] * steps
|
||||
else:
|
||||
raise TypeError('steps must be a list or int')
|
||||
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def setValue(self, data):
|
||||
if data != self._value:
|
||||
if data > self._maximum: data = self._maximum
|
||||
if data < self._minimum : data = self._minimum
|
||||
self._value = data
|
||||
self.valueChanged.emit(int(data))
|
||||
self.valueChanged[float].emit(float(data))
|
||||
self.update()
|
||||
|
||||
|
||||
def sizeHint(self):
|
||||
if self._vertical:
|
||||
return QSize(40, 120)
|
||||
else:
|
||||
return QSize(40, 20)
|
||||
|
||||
def minimum(self):
|
||||
return self._minimum
|
||||
|
||||
def maximum(self):
|
||||
return self._maximum
|
||||
|
||||
def setRange(self, low, hi):
|
||||
self._minimum = low
|
||||
self._maximum = hi
|
||||
|
||||
def setFormat(self, data):
|
||||
pass
|
||||
|
||||
def invertBool(self, value):
|
||||
if value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getInvertedAppearance(self):
|
||||
return self._opposite
|
||||
def setInvertedAppearance(self, data):
|
||||
self._opposite = bool(data)
|
||||
self.update()
|
||||
def resetInvertedAppearance(self, data):
|
||||
self._opposite = False
|
||||
self.update()
|
||||
|
||||
#########################################################################
|
||||
# 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 set_step_color_l(self, data):
|
||||
# convert stylesheet list of single string to list of multiple
|
||||
if ',' in data[0]:
|
||||
self.setSteps(data[0].split(','))
|
||||
else:
|
||||
self.setSteps(data)
|
||||
def get_step_color_l(self):
|
||||
return self._step_color_list
|
||||
def reset_step_color_l(self):
|
||||
self.setSteps( ['green', 'green', '#00b600', '#00b600',
|
||||
'#00d600', '#00d600', 'yellow', 'yellow', 'red', 'red'])
|
||||
|
||||
def getBackgroundColor(self):
|
||||
return self._background_color
|
||||
@Slot(QColor)
|
||||
def setBackgroundColor(self, value):
|
||||
self._background_color = value
|
||||
self.update()
|
||||
|
||||
def get_indicatorColor(self):
|
||||
return self._singleIndicatorColor
|
||||
@Slot(QColor)
|
||||
def set_indicatorColor(self, value):
|
||||
self._singleIndicatorColor = value
|
||||
self.update()
|
||||
def reset_indicatorColor(self, value):
|
||||
self._singleIndicatorColor = QColor('blue')
|
||||
self.update()
|
||||
|
||||
def getSplit(self):
|
||||
return int(self._split * 100)
|
||||
def setSplit(self, data):
|
||||
if data <0: data == 0
|
||||
elif data >100: data == 100
|
||||
self._split = data/100
|
||||
self.update()
|
||||
def resetSplit(self):
|
||||
self._split = .2
|
||||
self.update()
|
||||
|
||||
def getMax(self):
|
||||
return self._maximum
|
||||
def setMax(self, data):
|
||||
self._maximum = data
|
||||
def resetMax(self):
|
||||
self._maximum = 200
|
||||
|
||||
def getMin(self):
|
||||
return self._minimum
|
||||
def setMin(self, data):
|
||||
self._minimum = data
|
||||
def resetMin(self):
|
||||
self._minimum = 0
|
||||
|
||||
def getMultiColor(self):
|
||||
return self._useMultiColor
|
||||
def setMultiColor(self, data):
|
||||
self._useMultiColor = data
|
||||
self.update()
|
||||
def resetMultiColor(self):
|
||||
self._useMultiColor = False
|
||||
self.update()
|
||||
|
||||
def getVert(self):
|
||||
return self._vertical
|
||||
def setVert(self, data):
|
||||
self._vertical = data
|
||||
self.update()
|
||||
def resetVert(self):
|
||||
self._vertical = False
|
||||
self.update()
|
||||
|
||||
stepColorList = Property(
|
||||
'QStringList',
|
||||
get_step_color_l, set_step_color_l, reset_step_color_l)
|
||||
|
||||
backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor)
|
||||
indicatorColor = Property(QColor, get_indicatorColor, set_indicatorColor, reset_indicatorColor)
|
||||
useMultiColorIndicator = Property(bool, getMultiColor, setMultiColor, resetMultiColor)
|
||||
split = Property(int, getSplit, setSplit, resetSplit)
|
||||
setMaximum = Property(int, getMax, setMax, resetMax)
|
||||
setMinimum = Property(int, getMin, setMin, resetMin)
|
||||
setVertical = Property(bool, getVert, setVert, resetVert)
|
||||
setInverted = Property(bool, getInvertedAppearance, setInvertedAppearance, resetInvertedAppearance)
|
||||
|
||||
class HALPinType(enum.IntEnum):
|
||||
NONE = 0
|
||||
S32 = hal.HAL_S32
|
||||
FLOAT = hal.HAL_FLOAT
|
||||
|
||||
|
||||
class HalBar(Bar, _HalWidgetBase):
|
||||
HALPinType = HALPinType
|
||||
QEnum(HALPinType)
|
||||
|
||||
# older version of pyqt5 need this as well as QEnum
|
||||
NONE = 0
|
||||
S32 = hal.HAL_S32
|
||||
FLOAT = hal.HAL_FLOAT
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(). __init__( *args, **kwargs)
|
||||
self._pin_type = HALPinType.S32
|
||||
self._pin_name = ''
|
||||
self._invert_negative = False
|
||||
self._superOpposite = self._opposite
|
||||
|
||||
def _hal_init(self):
|
||||
if self._pin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name
|
||||
|
||||
self._superOpposite = self._opposite
|
||||
|
||||
if self._pin_type == HALPinType.FLOAT:
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_FLOAT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda data: self.updateDisplay(data))
|
||||
elif self._pin_type == HALPinType.S32:
|
||||
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):
|
||||
if data < 0 and self._invert_negative:
|
||||
self._opposite = self.invertBool(self._superOpposite)
|
||||
data = data * -1
|
||||
else:
|
||||
self._opposite = self._superOpposite
|
||||
self.setValue(data)
|
||||
|
||||
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
|
||||
|
||||
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_invert_negative(self, value):
|
||||
self._invert_negative = value
|
||||
def get_invert_negative(self):
|
||||
return self._invert_negative
|
||||
def reset_invert_negative(self):
|
||||
self._invert_negative = False
|
||||
|
||||
pinType = Property(HALPinType, get_pin_type, set_pin_type, reset_pin_type)
|
||||
pinName = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
invertOnNegative = Property(bool, get_invert_negative, set_invert_negative, reset_invert_negative)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from qtpy.QtWidgets import (QLabel, QSlider, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QPushButton, QCheckBox)
|
||||
|
||||
app = QtWidgets.QApplication([])
|
||||
w = QWidget()
|
||||
w.setGeometry(100, 100, 200, 100)
|
||||
w.setWindowTitle('Bar widget')
|
||||
layout = QVBoxLayout(w)
|
||||
|
||||
label = QLabel()
|
||||
|
||||
bar = HalBar()
|
||||
bar.setProperty('setInverted',True)
|
||||
bar.setProperty('setVertical',False)
|
||||
|
||||
#bar.setValue(51)
|
||||
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(200)
|
||||
slider.setSingleStep(10)
|
||||
slider.setPageStep(100)
|
||||
slider.valueChanged.connect(bar.setValue)
|
||||
slider.valueChanged.connect(label.setNum)
|
||||
slider.setValue(51)
|
||||
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(bar)
|
||||
layout.addWidget(slider)
|
||||
|
||||
button = QPushButton('Vertical')
|
||||
button.setCheckable(True)
|
||||
button.setChecked(bar.getVert())
|
||||
button.toggled.connect(bar.setVert)
|
||||
|
||||
layout.addWidget(button)
|
||||
|
||||
button = QPushButton('Use Multi Color')
|
||||
button.setCheckable(True)
|
||||
button.setChecked(bar.getMultiColor())
|
||||
button.toggled.connect(bar.setMultiColor)
|
||||
|
||||
layout.addWidget(button)
|
||||
|
||||
lyt = QHBoxLayout()
|
||||
check1 = QCheckBox('o%')
|
||||
check1.setAutoExclusive (True)
|
||||
check1.stateChanged.connect(lambda value: bar.setSplit(0))
|
||||
|
||||
check2 = QCheckBox('20%')
|
||||
check2.setAutoExclusive (True)
|
||||
check2.stateChanged.connect(lambda value: bar.setSplit(20))
|
||||
|
||||
check3 = QCheckBox('50%')
|
||||
check3.setAutoExclusive (True)
|
||||
check3.stateChanged.connect(lambda value: bar.setSplit(50))
|
||||
|
||||
lyt.addWidget(check1)
|
||||
lyt.addWidget(check2)
|
||||
lyt.addWidget(check3)
|
||||
layout.addLayout(lyt)
|
||||
|
||||
w.show()
|
||||
app.exec()
|
||||
|
||||
725
lib/python/qtvcp/widgets/basic_probe.py
Normal file
@@ -0,0 +1,725 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp basic probe
|
||||
#
|
||||
# Copyright (c) 2020 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# a probe screen based on ProbeBasic screen
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hal
|
||||
import json
|
||||
import re
|
||||
from qtpy.QtCore import QProcess, QRegularExpression, QFile, QEvent, Qt, Property
|
||||
from qtpy import QtGui, QtWidgets, uic, QtCore
|
||||
from qtpy.QtWidgets import QDialogButtonBox, QAbstractSlider, QLineEdit, QApplication
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Action, Status, Info, Path
|
||||
from qtvcp.widgets.dialogMixin import GeometryMixin
|
||||
from qtvcp import logger
|
||||
|
||||
ACTION = Action()
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
PATH = Path()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
current_dir = os.path.dirname(__file__)
|
||||
SUBPROGRAM = os.path.abspath(os.path.join(current_dir, 'probe_subprog.py'))
|
||||
CONFIG_DIR = os.getcwd()
|
||||
|
||||
# can use/favours local image and help files
|
||||
HELP = PATH.find_widget_path()
|
||||
|
||||
DEFAULT = 0
|
||||
WARNING = 1
|
||||
CRITICAL = 2
|
||||
|
||||
class BasicProbeParent(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(BasicProbeParent, self).__init__(parent)
|
||||
self.proc = None
|
||||
self._cmd = None
|
||||
self._runImmediately = True
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.hilightStyle = "border: 2px solid red;"
|
||||
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
self.valid = QtGui.QRegularExpressionValidator(QRegularExpression(r'^[+-]?((\d+(\.\d{,4})?)|(\.\d{,4}))$'))
|
||||
else:
|
||||
self.valid = QtGui.QRegularExpressionValidator(QRegularExpression(r'^[+-]?((\d+(\.\d{,3})?)|(\.\d{,3}))$'))
|
||||
self.setMinimumSize(600, 420)
|
||||
# load the widgets ui file
|
||||
self.filename = PATH.find_widget_path('basic_probe.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
# load the probe help file
|
||||
self.filename = PATH.find_widget_path('basic_probe_help.ui')
|
||||
try:
|
||||
self.dialog = uic.loadUi(self.filename)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
|
||||
self.probe_list = ["OUTSIDE CORNERS", "INSIDE CORNERS", "EDGE ANGLE", "BOSS and POCKETS",
|
||||
"RIDGE and VALLEY", "CALIBRATE"]
|
||||
self.status_list = ['xm', 'xc', 'xp', 'ym', 'yc', 'yp', 'lx', 'ly', 'z', 'd', 'a', 'delta']
|
||||
|
||||
#create parameter dictionary
|
||||
self.send_dict = {}
|
||||
# these parameters are sent to the subprogram
|
||||
self.parm_list = ['probe_diam',
|
||||
'rapid_vel',
|
||||
'search_vel',
|
||||
'probe_vel',
|
||||
'extra_depth',
|
||||
'latch_return_dist',
|
||||
'max_travel',
|
||||
'max_z_travel',
|
||||
'xy_clearance',
|
||||
'z_clearance',
|
||||
'side_edge_length',
|
||||
'diameter_hint',
|
||||
'x_hint_bp',
|
||||
'y_hint_bp',
|
||||
'x_hint_rv',
|
||||
'y_hint_rv',
|
||||
'calibration_offset',
|
||||
'cal_diameter',
|
||||
'cal_x_width',
|
||||
'cal_y_width',
|
||||
]
|
||||
self.nextlist = ['probe_tool']
|
||||
self.nextlist = self.nextlist + self.parm_list
|
||||
|
||||
# button connections
|
||||
self.cmb_probe_select.activated.connect(self.cmb_probe_select_changed)
|
||||
self.outside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.inside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.skew_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.boss_pocket_buttonGroup.buttonClicked.connect(self.boss_pocket_clicked)
|
||||
self.ridge_valley_buttonGroup.buttonClicked.connect(self.ridge_valley_clicked)
|
||||
self.cal_buttonGroup.buttonClicked.connect(self.cal_btn_clicked)
|
||||
self.clear_buttonGroup.buttonClicked.connect(self.clear_results_clicked)
|
||||
self.btn_probe_help.clicked.connect(self.probe_help_clicked)
|
||||
|
||||
self.cmb_probe_select.clear()
|
||||
self.cmb_probe_select.addItems(self.probe_list)
|
||||
self.stackedWidget_probe_buttons.setCurrentIndex(0)
|
||||
# define validators for all lineEdit widgets
|
||||
self.lineEdit_probe_tool.setValidator(QtGui.QRegularExpressionValidator(QRegularExpression('[0-9]{0,5}')))
|
||||
for i in self.parm_list:
|
||||
self['lineEdit_' + i].setValidator(self.valid)
|
||||
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data))
|
||||
|
||||
# catch focusIn event to pop calculator dialog
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.FocusIn:
|
||||
if isinstance(obj, QLineEdit):
|
||||
# only if mouse selected
|
||||
if event.reason () == 0:
|
||||
self._nextIndex = self.nextlist.index(obj.objectName().replace('lineEdit_',''))
|
||||
self.popEntry(obj)
|
||||
if self.dialog_code == 'CALCULATOR':
|
||||
obj.clearFocus()
|
||||
event.accept()
|
||||
return True
|
||||
return super(BasicProbeParent, self).eventFilter(obj, event)
|
||||
|
||||
# update the probe loaded HAL pin
|
||||
# can be used to inhibit the spindle
|
||||
def _tool_info(self, data):
|
||||
if data.id != -1:
|
||||
if data.id == int(self.lineEdit_probe_tool.text()):
|
||||
self.probe_loaded.set(True)
|
||||
else:
|
||||
self.probe_loaded.set(False)
|
||||
return
|
||||
self.probe_loaded.set(False)
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_status():
|
||||
return (STATUS.machine_is_on() and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
STATUS.connect('state_off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state_estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_status()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
# must directly initialize
|
||||
self.statuslabel_motiontype.hal_init()
|
||||
self.led_probe.hal_init()
|
||||
self.statelabel_machineUnits.hal_init()
|
||||
self.help = HelpDialog(self.QTVCP_INSTANCE_)
|
||||
self.help.hal_init(HAL_NAME='_basic_help')
|
||||
|
||||
# install event filters on all the lineedits
|
||||
# so we can call up a dialog when lineedit get focus
|
||||
for i in self.nextlist:
|
||||
self['lineEdit_' + i].installEventFilter(self)
|
||||
|
||||
self.set_checkableButtons(not self._runImmediately)
|
||||
|
||||
if self.PREFS_:
|
||||
tool = self.PREFS_.getpref('Probe tool', '0', str, 'PROBE OPTIONS')
|
||||
if not tool.isdecimal():
|
||||
tool = '-1'
|
||||
self.lineEdit_probe_tool.setText(tool)
|
||||
self.lineEdit_probe_diam.setText(self.PREFS_.getpref('Probe diameter', '4', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_rapid_vel.setText(self.PREFS_.getpref('Probe rapid', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_probe_vel.setText(self.PREFS_.getpref('Probe feed', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_search_vel.setText(self.PREFS_.getpref('Probe search', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_max_travel.setText(self.PREFS_.getpref('Probe max travel', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_max_z_travel.setText(self.PREFS_.getpref('Probe max z', '2', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_extra_depth.setText(self.PREFS_.getpref('Probe extra depth', '0', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_latch_return_dist.setText(self.PREFS_.getpref('Probe step off', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_xy_clearance.setText(self.PREFS_.getpref('Probe xy clearance', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_z_clearance.setText(self.PREFS_.getpref('Probe z clearance', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_side_edge_length.setText(self.PREFS_.getpref('Probe edge width', '10', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_calibration_offset.setText(self.PREFS_.getpref('Calibration offset', '0', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_cal_x_width.setText(self.PREFS_.getpref('Cal x width', '0', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_cal_y_width.setText(self.PREFS_.getpref('Cal y width', '0', str, 'PROBE OPTIONS'))
|
||||
self.lineEdit_cal_diameter.setText(self.PREFS_.getpref('Cal diameter', '0', str, 'PROBE OPTIONS'))
|
||||
|
||||
# make pins available for tool measure remaps
|
||||
oldname = self.HAL_GCOMP_.comp.getprefix()
|
||||
self.HAL_GCOMP_.comp.setprefix('qtbasicprobe')
|
||||
self.probe_loaded = self.HAL_GCOMP_.newpin("probe-loaded", hal.HAL_BIT, hal.HAL_OUT)
|
||||
self.HAL_GCOMP_.comp.setprefix(oldname)
|
||||
|
||||
# get current style of the line input
|
||||
self._oldstyle = self.lineEdit_probe_diam.styleSheet()
|
||||
|
||||
def _hal_cleanup(self):
|
||||
if self.PREFS_:
|
||||
LOG.debug('Saving Basic Probe data to preference file.')
|
||||
self.PREFS_.putpref('Probe tool', self.lineEdit_probe_tool.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe diameter', self.lineEdit_probe_diam.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe rapid', self.lineEdit_rapid_vel.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe feed', self.lineEdit_probe_vel.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe search', self.lineEdit_search_vel.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe max travel', self.lineEdit_max_travel.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe max z', self.lineEdit_max_z_travel.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe extra depth', self.lineEdit_extra_depth.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe step off', self.lineEdit_latch_return_dist.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe xy clearance', self.lineEdit_xy_clearance.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe z clearance', self.lineEdit_z_clearance.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Probe edge width', self.lineEdit_side_edge_length.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Calibration offset', self.lineEdit_calibration_offset.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Cal x width', self.lineEdit_cal_x_width.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Cal y width', self.lineEdit_cal_y_width.text(), str, 'PROBE OPTIONS')
|
||||
self.PREFS_.putpref('Cal diameter', self.lineEdit_cal_diameter.text(), str, 'PROBE OPTIONS')
|
||||
if self.proc is not None: self.proc.terminate()
|
||||
|
||||
# process the STATUS return message
|
||||
# set the line edit to the value if not cancelled
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
next = message.get('NEXT', False)
|
||||
back = message.get('BACK', False)
|
||||
if code and name:
|
||||
obj = message.get('OBJECT')
|
||||
if num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
if obj is self.lineEdit_probe_tool:
|
||||
obj.setText(str(int(num)))
|
||||
else:
|
||||
obj.setText(str(num))
|
||||
# clear high lighting
|
||||
obj.setStyleSheet(self._oldstyle)
|
||||
# request for next input widget from nextlist
|
||||
if next:
|
||||
newobj = self.findNext(self._nextIndex)
|
||||
# update the dialog
|
||||
self.popEntry(newobj,True)
|
||||
elif back:
|
||||
newobj = self.findBack(self._nextIndex)
|
||||
# update the dialog
|
||||
self.popEntry(newobj,True)
|
||||
|
||||
def findNext(self, num):
|
||||
while 1:
|
||||
self._nextIndex += 1
|
||||
if self._nextIndex == len(self.nextlist):
|
||||
self._nextIndex = 0
|
||||
newobj = self['lineEdit_{}'.format(self.nextlist[self._nextIndex])]
|
||||
if newobj.isVisible():
|
||||
break
|
||||
return newobj
|
||||
|
||||
def findBack(self, num):
|
||||
while 1:
|
||||
self._nextIndex -= 1
|
||||
if self._nextIndex == -1:
|
||||
self._nextIndex = len(self.nextlist)-1
|
||||
newobj = self['lineEdit_{}'.format(self.nextlist[self._nextIndex])]
|
||||
if newobj.isVisible():
|
||||
break
|
||||
return newobj
|
||||
|
||||
def popEntry(self, obj, next=False):
|
||||
STATUS.emit('focus-overlay-changed', False, None, None)
|
||||
obj.setStyleSheet(self.hilightStyle)
|
||||
QApplication.processEvents()
|
||||
|
||||
mess = {'NAME':self.dialog_code,
|
||||
'ID':'%s__' % self.objectName(),
|
||||
'OVERLAY':False,
|
||||
'OBJECT':obj,
|
||||
'TITLE':'Set Entry for {}'.format(obj.toolTip().upper()),
|
||||
'GEONAME':'_{}'.format(self.dialog_code),
|
||||
'OVERLAY':True,
|
||||
'NEXT':next,
|
||||
'WIDGETCYCLE': True
|
||||
}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
#################
|
||||
# process control
|
||||
#################
|
||||
def start_process(self):
|
||||
self.proc = QProcess()
|
||||
self.proc.setReadChannel(QProcess.StandardOutput)
|
||||
self.proc.started.connect(self.process_started)
|
||||
self.proc.readyReadStandardOutput.connect(self.read_stdout)
|
||||
self.proc.readyReadStandardError.connect(self.read_stderror)
|
||||
self.proc.finished.connect(self.process_finished)
|
||||
self.proc.start('python3 {}'.format(SUBPROGRAM))
|
||||
|
||||
def start_probe(self, cmd):
|
||||
if self.proc is not None:
|
||||
LOG.info("Probe Routine processor is busy")
|
||||
return
|
||||
ACTION.RECORD_CURRENT_MODE()
|
||||
t = int(self.lineEdit_probe_tool.text())
|
||||
if t != STATUS.get_current_tool():
|
||||
msg = "Probe tool # {}. not mounted in spindle".format(t)
|
||||
if not self.set_statusbar(msg,CRITICAL):
|
||||
STATUS.emit('update-machine-log', msg, 'TIME,CRITICAL')
|
||||
ACTION.SET_ERROR_MESSAGE(msg)
|
||||
return
|
||||
|
||||
self.start_process()
|
||||
string_to_send = cmd + '$' + json.dumps(self.send_dict) + '\n'
|
||||
#print("String to send ", string_to_send)
|
||||
STATUS.block_error_polling()
|
||||
self.proc.writeData(bytes(string_to_send, 'utf-8'))
|
||||
|
||||
def process_started(self):
|
||||
LOG.info("Basic_Probe subprogram started with PID {}\n".format(self.proc.processId()))
|
||||
|
||||
def read_stdout(self):
|
||||
qba = self.proc.readAllStandardOutput()
|
||||
line = qba.data()
|
||||
self.parse_input(line)
|
||||
|
||||
def read_stderror(self):
|
||||
qba = self.proc.readAllStandardError()
|
||||
line = qba.data()
|
||||
self.parse_input(line)
|
||||
|
||||
def process_finished(self, exitCode, exitStatus):
|
||||
LOG.info(("Probe Process finished - exitCode {} exitStatus {}".format(exitCode, exitStatus)))
|
||||
self.proc = None
|
||||
STATUS.unblock_error_polling()
|
||||
ACTION.RESTORE_RECORDED_MODE()
|
||||
|
||||
def parse_input(self, line):
|
||||
line = line.decode("utf-8")
|
||||
if "ERROR INFO" in line:
|
||||
ACTION.SET_ERROR_MESSAGE(line)
|
||||
elif "ERROR" in line:
|
||||
#print(line)
|
||||
STATUS.unblock_error_polling()
|
||||
ACTION.SET_ERROR_MESSAGE('Basic Probe process finished in error')
|
||||
elif "INFO" in line:
|
||||
pass
|
||||
elif "PROBE_ROUTINES" in line:
|
||||
if LOG.getEffectiveLevel() < logger.INFO:
|
||||
print(line)
|
||||
elif "COMPLETE" in line:
|
||||
STATUS.unblock_error_polling()
|
||||
LOG.info("Basic Probing routine completed without errors")
|
||||
return_data = line.rstrip().split('$')
|
||||
data = json.loads(return_data[1])
|
||||
self.show_results(data)
|
||||
elif "HISTORY" in line:
|
||||
if not self.set_statusbar(line, WARNING):
|
||||
STATUS.emit('update-machine-log', line, 'TIME')
|
||||
elif "DEBUG" in line:
|
||||
pass
|
||||
else:
|
||||
LOG.error("Error parsing return data from sub_processor. Line={}".format(line))
|
||||
|
||||
# return false if failed so other ways of reporting can be used.
|
||||
# there might not be a statusbar in main screen.
|
||||
def set_statusbar(self, msg, priority = DEFAULT, noLog = False):
|
||||
try:
|
||||
self.QTVCP_INSTANCE_.add_status(msg, priority, noLog)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Main button handler routines
|
||||
|
||||
# called externally to run selected routine
|
||||
# if not in _runImmediately mode
|
||||
def cycle_start(self):
|
||||
if self._runImmediately:
|
||||
self.set_statusbar('Basic probe set to run buttons immediately',WARNING)
|
||||
return
|
||||
if self._cmd is None:
|
||||
self.set_statusbar('No Basic probe probe selected',WARNING)
|
||||
return
|
||||
self.set_statusbar('Basic probe: Start Cycle: {}'.format(self._cmd.toolTip(),DEFAULT))
|
||||
self.get_parms()
|
||||
self.start_probe(self._cmd.property('probe'))
|
||||
|
||||
def probe_help_clicked(self):
|
||||
self.help.showDialog()
|
||||
|
||||
def cmb_probe_select_changed(self, index):
|
||||
self.stackedWidget_probe_buttons.setCurrentIndex(index)
|
||||
|
||||
if self._runImmediately:
|
||||
return
|
||||
# Auto exclusive doesn't allow unchecking all buttons
|
||||
# We force it here
|
||||
if not self._cmd is None:
|
||||
button = self._cmd
|
||||
button.group().setExclusive(False)
|
||||
button.blockSignals(True)
|
||||
button.setChecked(False)
|
||||
button.blockSignals(False)
|
||||
button.group().setExclusive(True)
|
||||
self._cmd = None
|
||||
|
||||
def probe_btn_clicked(self, button):
|
||||
self.generalButtonCall(button)
|
||||
|
||||
def boss_pocket_clicked(self, button):
|
||||
for i in ['x_hint_bp', 'y_hint_bp', 'diameter_hint']:
|
||||
if self['lineEdit_' + i].text() is None: return
|
||||
self.generalButtonCall(button)
|
||||
|
||||
def ridge_valley_clicked(self, button):
|
||||
for i in ['x_hint_rv', 'y_hint_rv']:
|
||||
if self['lineEdit_' + i].text() is None: return
|
||||
self.generalButtonCall(button)
|
||||
|
||||
def cal_btn_clicked(self, button):
|
||||
for i in ['calibration_offset', 'cal_diameter', 'cal_x_width', 'cal_y_width']:
|
||||
if self['lineEdit_' + i].text() is None: return
|
||||
self.generalButtonCall(button)
|
||||
|
||||
def generalButtonCall(self, button):
|
||||
cmd = button
|
||||
print("Button clicked ", cmd.property('probe'),self._cmd)
|
||||
|
||||
# run probe when buttons are pressed
|
||||
if self._runImmediately:
|
||||
self.set_statusbar('Basic probe: Start Cycle: {}'.format(cmd.toolTip(),DEFAULT))
|
||||
self.get_parms()
|
||||
self.start_probe(cmd.property('probe'))
|
||||
|
||||
# select probe when buttons pressed, run when start_cycle function called
|
||||
else:
|
||||
# Auto exclusive doesn't allow unchecking all buttons
|
||||
# We force it here
|
||||
if cmd == self._cmd:
|
||||
button.group().setExclusive(False)
|
||||
button.blockSignals(True)
|
||||
button.setChecked(False)
|
||||
button.blockSignals(False)
|
||||
button.group().setExclusive(True)
|
||||
self._cmd = None
|
||||
return
|
||||
self.set_statusbar('Basic probe: Selected: {}'.format(cmd.toolTip()),DEFAULT,noLog=True)
|
||||
self._cmd = cmd
|
||||
|
||||
def clear_results_clicked(self, button):
|
||||
cmd = button.property('clear')
|
||||
if cmd in dir(self): self[cmd]()
|
||||
|
||||
def clear_x(self):
|
||||
self.status_xm.setText('0')
|
||||
self.status_xp.setText('0')
|
||||
self.status_xc.setText('0')
|
||||
self.status_lx.setText('0')
|
||||
|
||||
def clear_y(self):
|
||||
self.status_ym.setText('0')
|
||||
self.status_yp.setText('0')
|
||||
self.status_yc.setText('0')
|
||||
self.status_ly.setText('0')
|
||||
|
||||
def clear_all(self):
|
||||
self.clear_x()
|
||||
self.clear_y()
|
||||
self.status_z.setText('0')
|
||||
self.status_d.setText('0')
|
||||
self.status_delta.setText('0')
|
||||
self.status_a.setText('0')
|
||||
|
||||
# Helper functions
|
||||
def get_parms(self):
|
||||
self.send_dict = {key: self['lineEdit_' + key].text() for key in (self.parm_list)}
|
||||
#print(self.send_dict)
|
||||
for key in ['allow_auto_zero', 'allow_auto_skew', 'cal_avg_error', 'cal_x_error', 'cal_y_error']:
|
||||
val = '1' if self[key].isChecked() else '0'
|
||||
self.send_dict.update( {key: val} )
|
||||
|
||||
def show_results(self, line):
|
||||
for key in self.status_list:
|
||||
if line[key] != 'None':
|
||||
self['status_' + key].setText(line[key])
|
||||
else:
|
||||
self['status_' + key].setText('')
|
||||
|
||||
# run-immediately buttons are momentary
|
||||
# pre-select buttons are checkable
|
||||
# focus policy changes for 'cycle start button steering'
|
||||
# with pre-select option
|
||||
def set_checkableButtons(self, state):
|
||||
if state:
|
||||
policy = Qt.ClickFocus
|
||||
else:
|
||||
policy = Qt.NoFocus
|
||||
self.setFocusPolicy(policy)
|
||||
for i in self.outside_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.inside_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.skew_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.ridge_valley_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.boss_pocket_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.cal_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
|
||||
#########################################################################
|
||||
# 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 set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialogCodeString = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_runImmediately(self, data):
|
||||
self._runImmediately = data
|
||||
self.set_checkableButtons(not data)
|
||||
def get_runImmediately(self):
|
||||
return self._runImmediately
|
||||
def reset_runImmediately(self):
|
||||
self._runImmediately = True
|
||||
|
||||
# toggle run on button push or run on function call
|
||||
runImmediately = Property(bool, get_runImmediately, set_runImmediately, reset_runImmediately)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
class HelpDialog(QtWidgets.QDialog, GeometryMixin):
|
||||
def __init__(self, parent=None):
|
||||
super(HelpDialog, self).__init__(parent)
|
||||
self._title = 'Basic Probe Help'
|
||||
self.setWindowFlags(self.windowFlags() | Qt.Tool |
|
||||
Qt.Dialog | Qt.WindowStaysOnTopHint |
|
||||
Qt.WindowSystemMenuHint)
|
||||
self.currentHelpPage=-1
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(600)
|
||||
self.helpPages = ['basic_help.html','basic_help1.html','basic_help2.html',
|
||||
'basic_help3.html','basic_help4.html','basic_help5.html',
|
||||
'basic_help6.html','basic_help7.html','basic_help8.html']
|
||||
|
||||
def _hal_init(self):
|
||||
self.buildWidget()
|
||||
self.set_default_geometry()
|
||||
self.read_preference_geometry('basicProbeHelpDialog-geometry')
|
||||
|
||||
def buildWidget(self):
|
||||
|
||||
l = QtWidgets.QVBoxLayout()
|
||||
t = QtWidgets.QTextEdit('Basic Probe Help')
|
||||
t.setReadOnly(True)
|
||||
l.addWidget(t)
|
||||
|
||||
buttons = QDialogButtonBox()
|
||||
|
||||
closebutton = QtWidgets.QPushButton()
|
||||
closebutton.setIconSize(QtCore.QSize(38, 38))
|
||||
closebutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/standardbutton-cancel-128.png'))
|
||||
closebutton.clicked.connect(lambda : self.close())
|
||||
|
||||
nextbutton = QtWidgets.QPushButton()
|
||||
nextbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
nextbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/right-32.png'))
|
||||
nextbutton.clicked.connect(lambda : self.next(t,True))
|
||||
|
||||
previousbutton = QtWidgets.QPushButton()
|
||||
previousbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
previousbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/left-32.png'))
|
||||
previousbutton.clicked.connect(lambda : self.next(t,False))
|
||||
|
||||
self.pageStepUpbutton = QtWidgets.QPushButton()
|
||||
self.pageStepUpbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
self.pageStepUpbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png'))
|
||||
self.pageStepUpbutton.clicked.connect(lambda : self.pageStep(t,False))
|
||||
|
||||
self.pageStepDwnbutton = QtWidgets.QPushButton()
|
||||
self.pageStepDwnbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
self.pageStepDwnbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/down-32.png'))
|
||||
self.pageStepDwnbutton.clicked.connect(lambda : self.pageStep(t,True))
|
||||
|
||||
bBox = QDialogButtonBox(buttons)
|
||||
bBox.addButton(self.pageStepUpbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(self.pageStepDwnbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(previousbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(nextbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(closebutton, QDialogButtonBox.DestructiveRole)
|
||||
bBox.rejected.connect(self.reject)
|
||||
|
||||
l.addWidget(bBox)
|
||||
self.setLayout(l)
|
||||
|
||||
# Load the first help page lazily (on first show): pre-scaling its
|
||||
# images at construction can hang or crash qtvcp headless.
|
||||
self._helpText = t
|
||||
self._helpLoaded = False
|
||||
|
||||
def _set_scaled_html(self, t, html):
|
||||
# QTextEdit scales raster images with a nearest-neighbour filter, which
|
||||
# makes the help diagrams look jagged. Pre-scale each raster <img> to
|
||||
# its requested size with a smooth filter and register it as a document
|
||||
# resource so Qt draws it 1:1. SVG images are left untouched so Qt's
|
||||
# vector renderer draws them crisply at any size.
|
||||
doc = t.document()
|
||||
for tag in re.findall(r'<img\b[^>]*>', html):
|
||||
match = re.search(r'src="([^"]+)"', tag)
|
||||
if match is None or match.group(1).lower().endswith('.svg'):
|
||||
continue
|
||||
src = match.group(1)
|
||||
image = QtGui.QImage(src)
|
||||
if image.isNull():
|
||||
continue
|
||||
width = re.search(r'width="(\d+)"', tag)
|
||||
height = re.search(r'height="(\d+)"', tag)
|
||||
if width is not None and int(width.group(1)) != image.width():
|
||||
image = image.scaledToWidth(int(width.group(1)), QtCore.Qt.SmoothTransformation)
|
||||
elif height is not None and int(height.group(1)) != image.height():
|
||||
image = image.scaledToHeight(int(height.group(1)), QtCore.Qt.SmoothTransformation)
|
||||
doc.addResource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(src), image)
|
||||
t.setHtml(html)
|
||||
|
||||
def next(self,t,direction=None):
|
||||
if direction is None:
|
||||
self.currentHelpPage = 0
|
||||
elif direction:
|
||||
self.currentHelpPage +=1
|
||||
if self.currentHelpPage > len(self.helpPages)-1:
|
||||
self.currentHelpPage = len(self.helpPages)-1
|
||||
else:
|
||||
self.currentHelpPage -=1
|
||||
if self.currentHelpPage < 0:
|
||||
self.currentHelpPage = 0
|
||||
try:
|
||||
pagePath = os.path.join(HELP, self.helpPages[self.currentHelpPage])
|
||||
file = QtCore.QFile(pagePath)
|
||||
file.open(QtCore.QFile.ReadOnly)
|
||||
html = file.readAll()
|
||||
html = str(html, encoding='utf8')
|
||||
html = html.replace("../images/widgets/","{}/widgets/".format(PATH.IMAGEDIR))
|
||||
self._set_scaled_html(t, html)
|
||||
if t.verticalScrollBar().isVisible():
|
||||
t.verticalScrollBar().setPageStep(100)
|
||||
self.pageStepDwnbutton.show()
|
||||
self.pageStepUpbutton.show()
|
||||
else:
|
||||
self.pageStepDwnbutton.hide()
|
||||
self.pageStepUpbutton.hide()
|
||||
|
||||
except Exception as e:
|
||||
t.setHtml('''
|
||||
<h1 style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:600;">Basic Probe Help not available</span> </h1>
|
||||
{}
|
||||
'''.format(e))
|
||||
if direction is None:
|
||||
return
|
||||
self.show()
|
||||
|
||||
def pageStep(self, t, state):
|
||||
if state:
|
||||
t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepAdd)
|
||||
else:
|
||||
t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepSub)
|
||||
|
||||
# accept button applies presets and if line number given starts linuxcnc
|
||||
def close(self):
|
||||
self.record_geometry()
|
||||
super(HelpDialog, self).close()
|
||||
|
||||
def showDialog(self):
|
||||
if not self._helpLoaded:
|
||||
try:
|
||||
self.next(self._helpText)
|
||||
except Exception as e:
|
||||
self._helpText.setText('Basic Probe Help file Unavailable:\n\n{}'.format(e))
|
||||
self._helpLoaded = True
|
||||
self.setWindowTitle(self._title);
|
||||
self.set_geometry()
|
||||
retval = self.exec()
|
||||
LOG.debug('Value of pressed button: {}'.format(retval))
|
||||
|
||||
# look for a custom version of basicProbe
|
||||
module = PATH.find_custom_widget_path('basic_probe.py','BasicProbeCustom')
|
||||
if not module:
|
||||
module = BasicProbeParent
|
||||
|
||||
class BasicProbe(module):
|
||||
def __init__(self, parent=None):
|
||||
super(BasicProbe, self).__init__(parent)
|
||||
|
||||
#############################
|
||||
# Testing #
|
||||
#############################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = BasicProbe()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
661
lib/python/qtvcp/widgets/calculator.py
Normal file
@@ -0,0 +1,661 @@
|
||||
#!/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())
|
||||
768
lib/python/qtvcp/widgets/camview_widget.py
Normal file
@@ -0,0 +1,768 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp camview
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# use open cv to do camera alignment
|
||||
|
||||
import os
|
||||
import _thread as Thread
|
||||
|
||||
import hal
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from qtpy.QtGui import QColor, QFont, QPainter, QPen, QImage
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
if __name__ != '__main__': # This avoids segfault when testing directly in python
|
||||
from qtvcp.core import Status, Info
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
#LOG.setLevel(logger.VERBOSE) # One of VERBOSE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
# Suppress cryptic messages when checking for useable ports
|
||||
os.environ["OPENCV_LOG_LEVEL"]="FATAL"
|
||||
|
||||
# If the library is missing don't crash the GUI
|
||||
# send an error and just make a blank widget.
|
||||
LIB_GOOD = True
|
||||
try:
|
||||
import cv2 as CV
|
||||
DEFAULT_API = CV.CAP_ANY
|
||||
except:
|
||||
LOG.error('Qtvcp Error with camview - is python3-opencv installed?')
|
||||
LIB_GOOD = False
|
||||
DEFAULT_API = 0 # CV.CAP_ANY
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
|
||||
class CamView(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(CamView, self).__init__(parent)
|
||||
self._qImageFormat=QImage.Format_RGB888
|
||||
self.video = None
|
||||
self.grabbed = None
|
||||
self.frame = None
|
||||
self._camNum = 0
|
||||
self.api = DEFAULT_API
|
||||
self.resolution=None
|
||||
self.diameter = 20
|
||||
self.rotation = 0
|
||||
self.rotationIncrement = .5
|
||||
self.scale = 1
|
||||
self.scaleX = 1.0
|
||||
self.scaleY = 1.0
|
||||
self.gap = 5
|
||||
self._noRotate = False
|
||||
self.setWindowTitle('Cam View')
|
||||
self.setGeometry(100, 100, 200, 200)
|
||||
self.text_color = QColor(255, 255, 255)
|
||||
self.circle_color = QtCore.Qt.red
|
||||
self.cross_color = QtCore.Qt.yellow
|
||||
self.cross_pointer_color = QtCore.Qt.white
|
||||
self.font = QFont("arial,helvetica", 40)
|
||||
self.SUB = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
|
||||
if LIB_GOOD:
|
||||
self.text = 'No Image'
|
||||
else:
|
||||
self.text = 'Missing\npython-opencv\nLibrary'
|
||||
self.pix = None
|
||||
self.degree = str("\N{DEGREE SIGN}")
|
||||
|
||||
# trap so can run script directly to test
|
||||
try:
|
||||
if INFO.PROGRAM_PREFIX is not None:
|
||||
self.user_path = os.path.expanduser(INFO.PROGRAM_PREFIX)
|
||||
except:
|
||||
self.user_path = (os.path.join(os.path.expanduser('~'), 'linuxcnc/nc_files'))
|
||||
|
||||
#self.blobInit()
|
||||
|
||||
def _hal_init(self):
|
||||
try:
|
||||
self.pin_ = self.HAL_GCOMP_.newpin('cam-rotation',hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
except:
|
||||
pass
|
||||
if LIB_GOOD:
|
||||
STATUS.connect('periodic', self.nextFrameSlot)
|
||||
|
||||
##################################
|
||||
# no button scroll = circle diameter
|
||||
# left button scroll = zoom
|
||||
# right button scroll = cross hair rotation
|
||||
##################################
|
||||
def wheelEvent(self, event):
|
||||
super(CamView, self).wheelEvent(event)
|
||||
mouse_state = QtWidgets.QApplication.mouseButtons()
|
||||
if event.angleDelta().y() < 0:
|
||||
if mouse_state == QtCore.Qt.NoButton:
|
||||
self.diameter -= self.rotationIncrement
|
||||
if mouse_state == QtCore.Qt.LeftButton:
|
||||
self.scale -= .1
|
||||
if mouse_state == QtCore.Qt.RightButton:
|
||||
if not self._noRotate:
|
||||
self.rotation += self.rotationIncrement
|
||||
else:
|
||||
if mouse_state == QtCore.Qt.NoButton:
|
||||
self.diameter += self.rotationIncrement
|
||||
if mouse_state == QtCore.Qt.LeftButton:
|
||||
self.scale += .1
|
||||
if mouse_state == QtCore.Qt.RightButton:
|
||||
if not self._noRotate:
|
||||
self.rotation -= self.rotationIncrement
|
||||
self.limitChecks()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() & QtCore.Qt.LeftButton:
|
||||
if not self._noRotate:
|
||||
self.rotation += self.rotationIncrement
|
||||
self.limitChecks()
|
||||
elif event.button() & QtCore.Qt.RightButton:
|
||||
if not self._noRotate:
|
||||
self.rotation -= self.rotationIncrement
|
||||
self.limitChecks()
|
||||
elif event.button() & QtCore.Qt.MiddleButton:
|
||||
self.rotation_increments_changed()
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() & QtCore.Qt.LeftButton:
|
||||
self.scale = 1
|
||||
elif event.button() & QtCore.Qt.RightButton:
|
||||
self.rotation = 0
|
||||
elif event.button() & QtCore.Qt.MiddleButton:
|
||||
self.diameter = 20
|
||||
|
||||
def zoom_in(self):
|
||||
if self.scale >= 5:
|
||||
return
|
||||
self.scale += 0.1
|
||||
|
||||
def zoom_out(self, event):
|
||||
if self.scale <= 1:
|
||||
return
|
||||
self.scale -= 0.1
|
||||
|
||||
def limitChecks(self):
|
||||
w = self.size().width()
|
||||
if self.diameter < 2: self.diameter = 2
|
||||
if self.diameter > w: self.diameter = w
|
||||
if self.rotation > 360 - self.rotationIncrement: self.rotation = 0
|
||||
if self.rotation < 0: self.rotation = 360 - self.rotationIncrement
|
||||
if self.scale < 1: self.scale = 1
|
||||
if self.scale > 5: self.scale = 5
|
||||
|
||||
def nextFrameSlot(self, w):
|
||||
if not self.video: return
|
||||
if not self.isVisible(): return
|
||||
|
||||
############################
|
||||
# capture a freme from cam
|
||||
############################
|
||||
ret, frame = self.video.read()
|
||||
if not ret: return
|
||||
|
||||
# set digital zoom
|
||||
frame = self.zoom(frame, self.scale)
|
||||
|
||||
# invert image
|
||||
frame = self.flip(frame)
|
||||
|
||||
# make a Q image
|
||||
self.pix = self.makeImage(frame, self._qImageFormat)
|
||||
|
||||
# make a CV window for testing
|
||||
#self.makeCVImage(self.canny(self.convertToGray(frame)))
|
||||
|
||||
# repaint the window
|
||||
self.update()
|
||||
try:
|
||||
self.pin_.set(self.rotation)
|
||||
except:
|
||||
pass
|
||||
|
||||
def convertToRGB(self, img):
|
||||
return CV.cvtColor(img, CV.COLOR_BGR2RGB)
|
||||
|
||||
# new qt 5.13 + for QImage.Format_Grayscale16
|
||||
def convertToGray(self, img):
|
||||
return CV.cvtColor(img, CV.COLOR_BGR2GRAY)
|
||||
|
||||
def blur(self, img, B=7):
|
||||
return CV.GaussingBlur(img, (B,B), CV.BORDER_DEFAULT)
|
||||
|
||||
def canny(self, img, x=125, y=175):
|
||||
return CV.Canny(img , x, y)
|
||||
|
||||
def makeImage(self, image, qFormat=QImage.Format_RGB888):
|
||||
img = self.convertToRGB(image)
|
||||
return QImage(img, img.shape[1], img.shape[0], img.strides[0], qFormat)
|
||||
|
||||
# openCV Window
|
||||
def makeCVImage(self, frame):
|
||||
CV.imshow('CV Image',frame)
|
||||
|
||||
def rescaleFrame(self, frame, scale=1, scale_x=1.0, scale_y=1.0):
|
||||
x = abs(scale_x * scale)
|
||||
y = abs(scale_y * scale)
|
||||
return CV.resize(frame, None, fx = x, fy = y, interpolation=CV.INTER_CUBIC)
|
||||
|
||||
def zoom(self, frame, scale):
|
||||
# get original size of image
|
||||
(oh, ow) = frame.shape[:2]
|
||||
#############################
|
||||
# scale image
|
||||
#############################
|
||||
frame = self.rescaleFrame(frame, scale,self.scaleX,self.scaleY)
|
||||
##########################
|
||||
# crop to the original size of the frame
|
||||
# measure from center, so we zoom on center
|
||||
# ch = center of current height
|
||||
# coh = center of original height
|
||||
##########################
|
||||
(h, w) = frame.shape[:2]
|
||||
ch = int(h/2)
|
||||
cw = int(w/2)
|
||||
coh = int(oh/2)
|
||||
cow = int(ow/2)
|
||||
# NOTE: its img[y: y + h, x: x + w]
|
||||
return frame[ch-coh:ch+coh, cw-cow:cw+cow]
|
||||
|
||||
# flip the image based on if scaleX and scaleY are negative
|
||||
def flip(self, image):
|
||||
if self.scaleX > 0 and self.scaleY > 0:
|
||||
return image
|
||||
elif self.scaleX < 0 and self.scaleY < 0:
|
||||
flip = -1
|
||||
elif self.scaleX < 0:
|
||||
flip = 0
|
||||
else:
|
||||
flip = 1
|
||||
return CV.flip(image, flip)
|
||||
|
||||
# draw a circle around small holes
|
||||
#
|
||||
def findCircles(self,frame):
|
||||
# Our operations on the frame come here
|
||||
gray = CV.cvtColor(frame, CV.COLOR_BGR2GRAY)
|
||||
# Display the resulting frame
|
||||
|
||||
circles = CV.HoughCircles(gray,CV.cv.CV_HOUGH_GRADIENT,1,20,param1=50,param2=30,minRadius=10,maxRadius=15)
|
||||
# print circles
|
||||
if circles is not None:
|
||||
#print circles
|
||||
circles = np.uint16(np.around(circles))
|
||||
for i in circles[0,:]:
|
||||
# draw the outer circle
|
||||
CV.circle(gray,(i[0],i[1]),i[2],(246,11,11),1)
|
||||
# draw the center of the circle
|
||||
CV.circle(gray,(i[0],i[1]),2,(246,11,11),1)
|
||||
|
||||
CV.imshow('Circles',gray)
|
||||
|
||||
def blobInit(self):
|
||||
# Setup BlobDetector
|
||||
detector = CV.SimpleBlobDetector()
|
||||
params = CV.SimpleBlobDetector_Params()
|
||||
|
||||
# Filter by Area.
|
||||
params.filterByArea = True
|
||||
params.minArea = 20000
|
||||
params.maxArea = 40000
|
||||
|
||||
# Filter by Circularity
|
||||
params.filterByCircularity = True
|
||||
params.minCircularity = 0.5
|
||||
|
||||
# Filter by Convexity
|
||||
params.filterByConvexity = False
|
||||
#params.minConvexity = 0.87
|
||||
|
||||
# Filter by Inertia
|
||||
params.filterByInertia = True
|
||||
params.minInertiaRatio = 0.8
|
||||
|
||||
# Distance Between Blobs
|
||||
params.minDistBetweenBlobs = 200
|
||||
|
||||
# Create a detector with the parameters
|
||||
self.detector = CV.SimpleBlobDetector(params)
|
||||
|
||||
# find circles overlay and draw a cross
|
||||
# https://www.kurokesu.com/main/2016/07/25/advanced-opencv-3-python-hole-detection/
|
||||
def findBlob(self, image):
|
||||
overlay = image.copy()
|
||||
|
||||
keypoints = self.detector.detect(image)
|
||||
for k in keypoints:
|
||||
CV.circle(overlay, (int(k.pt[0]), int(k.pt[1])), int(k.size/2), (0, 0, 255), -1)
|
||||
CV.line(overlay, (int(k.pt[0])-20, int(k.pt[1])), (int(k.pt[0])+20, int(k.pt[1])), (0,0,0), 3)
|
||||
CV.line(overlay, (int(k.pt[0]), int(k.pt[1])-20), (int(k.pt[0]), int(k.pt[1])+20), (0,0,0), 3)
|
||||
|
||||
opacity = 0.5
|
||||
CV.addWeighted(overlay, opacity, image, 1 - opacity, 0, image)
|
||||
|
||||
# Uncomment to resize to fit output window if needed
|
||||
#image = CV.resize(im, None,fx=0.5, fy=0.5, interpolation = CV.INTER_CUBIC)
|
||||
CV.imshow("Output", image)
|
||||
|
||||
def showEvent(self, event):
|
||||
if LIB_GOOD:
|
||||
if self.video is None:
|
||||
try:
|
||||
self.video = WebcamVideoStream(src=self._camNum, api=self.api, res = self.resolution )
|
||||
if not self.video.isOpened():
|
||||
p = self.video.list_ports(api=self.api)[1]
|
||||
self.text = 'Error with video {}\nAvailable ports:\n{}'.format(self._camNum, p)
|
||||
else:
|
||||
self.video.start()
|
||||
except Exception as e:
|
||||
LOG.error('Video capture error: {}'.format(e))
|
||||
else:
|
||||
self.video.start()
|
||||
|
||||
def hideEvent(self, event):
|
||||
if LIB_GOOD:
|
||||
try:
|
||||
self.video.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
def resizeEvent(self, event):
|
||||
# Create a square base size of 10x10 and scale it to the new size
|
||||
# maintaining aspect ratio.
|
||||
new_size = QtCore.QSize(10, 10)
|
||||
new_size.scale(event.size(), QtCore.Qt.KeepAspectRatio)
|
||||
self.resize(new_size)
|
||||
|
||||
def paintEvent(self, event):
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
if self.pix:
|
||||
qp.drawImage(self.rect(), self.pix)
|
||||
self.drawText(event, qp)
|
||||
self.drawCircle(event, qp)
|
||||
self.drawCrossHair(event, qp)
|
||||
qp.end()
|
||||
|
||||
def drawText(self, event, qp):
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
qp.setPen(self.text_color)
|
||||
qp.setFont(self.font)
|
||||
if self._noRotate:
|
||||
inc =''
|
||||
else:
|
||||
inc = str(self.rotationIncrement).translate(self.SUB)
|
||||
if self.pix:
|
||||
qp.drawText(self.rect(), QtCore.Qt.AlignTop, '{:.1f}{} {:>}'.format(self.rotation,self.degree,
|
||||
inc))
|
||||
else:
|
||||
qp.drawText(self.rect(), QtCore.Qt.AlignCenter, self.text)
|
||||
|
||||
def drawCircle(self, event, gp):
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
radx = self.diameter/2
|
||||
rady = self.diameter/2
|
||||
# draw red circles
|
||||
gp.setPen(self.circle_color)
|
||||
center = QtCore.QPoint(w//2, h//2)
|
||||
gp.drawEllipse(center, radx, rady)
|
||||
|
||||
def drawCrossHair(self, event, gp):
|
||||
size = self.size()
|
||||
w = size.width()//2
|
||||
h = size.height()//2
|
||||
pen0 = QPen(self.cross_pointer_color, 1, QtCore.Qt.SolidLine)
|
||||
pen = QPen(self.cross_color, 1, QtCore.Qt.SolidLine)
|
||||
gp.translate(w, h)
|
||||
gp.rotate(-self.rotation)
|
||||
gp.setPen(pen0)
|
||||
gp.drawLine(0, 0-self.gap, 0, -h)
|
||||
gp.setPen(pen)
|
||||
gp.drawLine(-w, 0, 0-self.gap, 0)
|
||||
gp.drawLine(0+self.gap, 0, w, 0)
|
||||
gp.drawLine(0, 0+self.gap, 0, h)
|
||||
|
||||
def rotation_increments_changed(self,w=None):
|
||||
if self.rotationIncrement == 5.00:
|
||||
self.rotationIncrement = 1
|
||||
elif self.rotationIncrement == 1:
|
||||
self.rotationIncrement = 0.5
|
||||
elif self.rotationIncrement == 0.5:
|
||||
self.rotationIncrement = 0.1
|
||||
|
||||
else:
|
||||
self.rotationIncrement = 5
|
||||
|
||||
def setCircleColor(self, color):
|
||||
self.circle_color = color
|
||||
|
||||
def setCrossColor(self, color):
|
||||
self.cross_color = color
|
||||
|
||||
def setPointerColor(self, color):
|
||||
self.cross_pointer_color = color
|
||||
|
||||
def saveImage(self):
|
||||
filepath = '{}/camImage.png'.format(self.user_path)
|
||||
self.video.writeFrame(filepath)
|
||||
|
||||
# if data is a string or int, search for name/int in library and use it.
|
||||
def setAPI(self, data):
|
||||
if not LIB_GOOD:
|
||||
return
|
||||
camera_backends = CV.videoio_registry.getCameraBackends()
|
||||
|
||||
if isinstance(data,str):
|
||||
# should we use any backend option?
|
||||
if data in ('','ANY','DEFAULT'):
|
||||
self.api = 0
|
||||
return
|
||||
# is it a string of a number?
|
||||
if data.isdigit():
|
||||
if int(data) in camera_backends:
|
||||
self.api = int(data)
|
||||
else:
|
||||
LOG.warning(f'Backend {data} not found.')
|
||||
return
|
||||
# ok it's a name
|
||||
for backend in camera_backends:
|
||||
if CV.videoio_registry.getBackendName(backend) == data:
|
||||
self.api = backend
|
||||
break
|
||||
else:
|
||||
LOG.warning(f'Backend {data} not found.')
|
||||
|
||||
elif isinstance(data,int):
|
||||
if data in camera_backends:
|
||||
self.api = data
|
||||
else:
|
||||
LOG.warning(f'Backend {data} not found.')
|
||||
|
||||
# returns the api backend name from backend int
|
||||
def getAPIName(self, num):
|
||||
return CV.videoio_registry.getBackendName(self.api)
|
||||
|
||||
# sets the resolution tuple (width,height) from a string 'width,height'
|
||||
def setResolution(self, data):
|
||||
if isinstance(data,str):
|
||||
if not data.upper() in('','DEFAULT','NONE'):
|
||||
data = tuple(int(x) for x in data.split(','))
|
||||
self.resolution = data
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# These can also be set as WIDGET.setProperty('property_name', data)
|
||||
########################################################################
|
||||
|
||||
|
||||
def set_wheel_rotation(self, value):
|
||||
self._noRotate = value
|
||||
def get_wheel_rotation(self):
|
||||
return self._noRotate
|
||||
def reset_wheel_rotation(self):
|
||||
self._noRotate = False
|
||||
def set_camnum(self, value):
|
||||
self._camNum = value
|
||||
def get_camnum(self):
|
||||
return self._camNum
|
||||
def reset_camnum(self):
|
||||
self._camNum = 0
|
||||
# designer will show these properties in this order:
|
||||
block_wheel_rotation = QtCore.Property(bool, get_wheel_rotation, set_wheel_rotation, reset_wheel_rotation)
|
||||
camera_number = QtCore.Property(int, get_camnum, set_camnum, reset_camnum)
|
||||
|
||||
class WebcamVideoStream:
|
||||
def __init__(self, src=0, api=DEFAULT_API, res=None):
|
||||
self.possible_resolutions = []
|
||||
# initialize the video camera stream and read the first frame
|
||||
# from the stream
|
||||
|
||||
flag = True
|
||||
camera_backends = CV.videoio_registry.getCameraBackends()
|
||||
for backend in camera_backends:
|
||||
if backend == api: flag = False
|
||||
LOG.verbose('Available backend library:{}'.format(CV.videoio_registry.getBackendName(backend)))
|
||||
if flag and api != CV.CAP_ANY:
|
||||
LOG.error('OpenCV backend library {} not available'.format(CV.videoio_registry.getBackendName(api)))
|
||||
|
||||
LOG.debug("Using {} backend".format(CV.videoio_registry.getBackendName(api)))
|
||||
|
||||
self.stream = self.openStream(src, api, res)
|
||||
|
||||
if not (self.stream.isOpened()):
|
||||
self.stream.release()
|
||||
LOG.error('Could not open video device {}'.format(src))
|
||||
|
||||
# get a list of ports
|
||||
plist = self.list_ports(api)[1]
|
||||
if src not in plist:
|
||||
LOG.error('port number {}, is not a working port- trying: {}'.format(src,plist[0]))
|
||||
# try again with a hopefully functioning port
|
||||
if plist != []:
|
||||
self.stream = self.openStream(plist[0], api, res)
|
||||
|
||||
|
||||
width = int(self.stream.get(CV.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(self.stream.get(CV.CAP_PROP_FRAME_HEIGHT))
|
||||
LOG.info(f"Current camera resolution: {width}x{height}")
|
||||
|
||||
# initialize the variable used to indicate if the thread should
|
||||
# be stopped
|
||||
self.stopped = False
|
||||
self.grabbed = None
|
||||
self.frame = None
|
||||
|
||||
def isOpened(self):
|
||||
try:
|
||||
return self.stream.isOpened()
|
||||
except:
|
||||
return False
|
||||
|
||||
def openStream(self, src, api, res=None):
|
||||
# initialize the video camera stream and read the first frame
|
||||
# from the stream
|
||||
try:
|
||||
stream = CV.VideoCapture(src, api)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
# try using an older function signature
|
||||
stream = CV.VideoCapture(src)
|
||||
|
||||
# seems only val2 is successful changing resolutions
|
||||
if not api == CV.CAP_ANY:
|
||||
w,h = self.getBestResolution(stream)
|
||||
# look for requested resolution
|
||||
if not res is None and res in self.possible_resolutions:
|
||||
w,h=res
|
||||
stream.set(CV.CAP_PROP_FRAME_WIDTH, w)
|
||||
stream.set(CV.CAP_PROP_FRAME_HEIGHT, h)
|
||||
|
||||
# last ditch - sometimes the default resolution can not be changed
|
||||
# if the height is 0 - failed, so just try default
|
||||
actual_height = int(stream.get(CV.CAP_PROP_FRAME_HEIGHT))
|
||||
if actual_height == 0:
|
||||
stream.release()
|
||||
stream = CV.VideoCapture(src, api)
|
||||
|
||||
return stream
|
||||
|
||||
def getBestResolution(self, stream):
|
||||
resolutions_to_test = [
|
||||
(320, 240), (640, 480), (800, 600), (1024, 768),
|
||||
(1280, 720), (1920, 1080)
|
||||
]
|
||||
start_width = int(stream.get(CV.CAP_PROP_FRAME_WIDTH))
|
||||
start_height = int(stream.get(CV.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
for width, height in resolutions_to_test:
|
||||
stream.set(CV.CAP_PROP_FRAME_WIDTH, width)
|
||||
stream.set(CV.CAP_PROP_FRAME_HEIGHT, height)
|
||||
actual_width = int(stream.get(CV.CAP_PROP_FRAME_WIDTH))
|
||||
actual_height = int(stream.get(CV.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
if actual_width == width and actual_height == height:
|
||||
if not (actual_width,actual_height) in self.possible_resolutions:
|
||||
self.possible_resolutions.append((actual_width,actual_height))
|
||||
elif actual_width !=0 and actual_height !=0:
|
||||
self.possible_resolutions.append((actual_width,actual_height))
|
||||
else:
|
||||
LOG.verbose(f"Resolution {width}x{height} is NOT supported (actual: {actual_width}x{actual_height}).")
|
||||
|
||||
LOG.verbose("All possible resolutions found:")
|
||||
for res in self.possible_resolutions:
|
||||
LOG.verbose(res)
|
||||
try:
|
||||
best = self.possible_resolutions[len(self.possible_resolutions)-1]
|
||||
LOG.debug('Best resolution:{}'.format(best))
|
||||
except:
|
||||
return (start_width,start_height)
|
||||
return best
|
||||
|
||||
def start(self):
|
||||
# start the thread to read frames from the video stream
|
||||
self.stopped = False
|
||||
Thread.start_new_thread(self._update, ())
|
||||
|
||||
return self
|
||||
|
||||
def _update(self):
|
||||
# keep looping infinitely until the thread is stopped
|
||||
while True:
|
||||
# if the thread indicator variable is set, stop the thread
|
||||
if self.stopped:
|
||||
return
|
||||
# otherwise, read the next frame from the stream
|
||||
(self.grabbed, self.frame) = self.stream.read()
|
||||
|
||||
def read(self):
|
||||
# return the frame most recently read
|
||||
return (self.grabbed, self.frame)
|
||||
|
||||
def stop(self):
|
||||
# indicate that the thread should be stopped
|
||||
self.stopped = True
|
||||
|
||||
# TODO path checking
|
||||
def writeFrame(self, filepath):
|
||||
CV.imwrite(filepath, self.frame)
|
||||
print('saved camview image to: {}'.format(filepath))
|
||||
|
||||
def list_ports(self,api):
|
||||
"""
|
||||
Test the ports and returns a tuple with the available ports and the ones that are working.
|
||||
"""
|
||||
non_working_ports = []
|
||||
dev_port = 0
|
||||
working_ports = []
|
||||
available_ports = []
|
||||
while len(non_working_ports) < 6: # if there are more than 5 non working ports stop the testing.
|
||||
camera = CV.VideoCapture(dev_port, api)
|
||||
if not camera.isOpened():
|
||||
non_working_ports.append(dev_port)
|
||||
LOG.debug("Port %s is not working." %dev_port)
|
||||
else:
|
||||
is_reading, img = camera.read()
|
||||
w = camera.get(3)
|
||||
h = camera.get(4)
|
||||
if is_reading:
|
||||
LOG.debug("Port %s is working and reads images (%s x %s)" %(dev_port,w,h))
|
||||
working_ports.append(dev_port)
|
||||
else:
|
||||
LOG.debug("Port %s for camera ( %s x %s) is present but does not read." %(dev_port,w,h))
|
||||
available_ports.append(dev_port)
|
||||
dev_port +=1
|
||||
camera.release()
|
||||
return available_ports,working_ports,non_working_ports
|
||||
|
||||
class CamAngle(CamView):
|
||||
def __init__(self, parent=None):
|
||||
super(CamAngle, self).__init__(parent)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
if event.button() & QtCore.Qt.LeftButton:
|
||||
# zoom
|
||||
self.scale = 1
|
||||
elif event.button() & QtCore.Qt.MiddleButton:
|
||||
self.diameter = 40
|
||||
|
||||
def wheelEvent(self, event):
|
||||
mouse_state = QtWidgets.QApplication.mouseButtons()
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
if event.angleDelta().y() < 0:
|
||||
if mouse_state == QtCore.Qt.NoButton:
|
||||
self.diameter -= 2
|
||||
if mouse_state == QtCore.Qt.LeftButton:
|
||||
self.scale -= .1
|
||||
else:
|
||||
if mouse_state == QtCore.Qt.NoButton:
|
||||
self.diameter += 2
|
||||
if mouse_state == QtCore.Qt.LeftButton:
|
||||
self.scale += .1
|
||||
if self.diameter < 2: self.diameter = 2
|
||||
if self.diameter > w: self.diameter = w
|
||||
if self.scale < 1: self.scale = 1
|
||||
if self.scale > 5: self.scale = 5
|
||||
|
||||
def drawText(self, event, qp):
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
qp.setPen(self.text_color)
|
||||
qp.setFont(self.font)
|
||||
if self.pix:
|
||||
angle = 0.0 if self.rotation == 0 else 360 - self.rotation
|
||||
qp.drawText(self.rect(), QtCore.Qt.AlignTop, '{:0.3f}{}'.format(angle, self.degree))
|
||||
else:
|
||||
qp.drawText(self.rect(), QtCore.Qt.AlignCenter, self.text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
import sys
|
||||
from qtpy.QtWidgets import (QLabel, QSlider, QDial,QWidget, QVBoxLayout,
|
||||
QHBoxLayout)
|
||||
from qtpy.QtCore import (Qt)
|
||||
|
||||
def hDialMoved():
|
||||
print("Dial value = %i" % (hdial.value()))
|
||||
capture.scaleX = hdial.value()/100
|
||||
|
||||
def vDialMoved():
|
||||
print("VDial value = %i" % (vdial.value()))
|
||||
capture.scaleY = vdial.value()/100
|
||||
|
||||
def setScale():
|
||||
print("slider value = %i" % (slider.value()))
|
||||
capture.scale = slider.value()/100
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = QWidget()
|
||||
w.setGeometry(100, 100, 400, 200)
|
||||
w.setWindowTitle('Bar widget')
|
||||
layout = QVBoxLayout(w)
|
||||
hlyt = QHBoxLayout()
|
||||
|
||||
capture = CamAngle()
|
||||
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.setMinimum(100)
|
||||
slider.setMaximum(400)
|
||||
slider.setSingleStep(10)
|
||||
slider.setPageStep(100)
|
||||
slider.valueChanged.connect(setScale)
|
||||
slider.setValue(100)
|
||||
|
||||
hdial = QDial()
|
||||
hdial.setMinimum(100)
|
||||
hdial.setMaximum(200)
|
||||
hdial.setValue(100)
|
||||
hdial.valueChanged.connect(hDialMoved)
|
||||
|
||||
vdial = QDial()
|
||||
vdial.setMinimum(100)
|
||||
vdial.setMaximum(200)
|
||||
vdial.setValue(100)
|
||||
vdial.valueChanged.connect(vDialMoved)
|
||||
|
||||
hlyt.addWidget(capture)
|
||||
hlyt.addWidget(hdial)
|
||||
hlyt.addWidget(vdial)
|
||||
|
||||
layout.addLayout(hlyt)
|
||||
layout.addWidget(slider)
|
||||
|
||||
def jump():
|
||||
capture.nextFrameSlot(None)
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.timeout.connect(jump)
|
||||
timer.start(100)
|
||||
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
221
lib/python/qtvcp/widgets/container_widgets.py
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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 os
|
||||
import linuxcnc
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtpy.QtCore import Property
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
class StateEnableGridLayout(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StateEnableGridLayout, self).__init__(parent)
|
||||
# if 'NO_FORCE_HOMING' is true, MDI commands are allowed before homing.
|
||||
self.inifile = os.environ.get('INI_FILE_NAME', '/dev/null')
|
||||
self.ini = linuxcnc.ini(self.inifile)
|
||||
self.no_home_required = self.ini.getbool("TRAJ", "NO_FORCE_HOMING", fallback=False)
|
||||
self.is_on = False
|
||||
self.is_homed = False
|
||||
self.is_idle = False
|
||||
self.is_not_idle = False
|
||||
self.setEnabled(False)
|
||||
|
||||
def _hal_init(self):
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
if self.is_on:
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
if self.is_homed:
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
if self.is_idle:
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(False))
|
||||
elif self.is_not_idle:
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(True))
|
||||
if not (self.is_idle or self.is_not_idle or self.is_on or self.is_homed):
|
||||
STATUS.connect('state-estop-reset', lambda w: self.setEnabled(True))
|
||||
else:
|
||||
STATUS.connect('interp-idle', self.idle_check)
|
||||
|
||||
def idle_check(self, w):
|
||||
state = True
|
||||
if self.is_homed:
|
||||
state = state and (STATUS.is_all_homed() or self.no_home_required)
|
||||
if self.is_on:
|
||||
state = state and STATUS.machine_is_on()
|
||||
if self.is_idle:
|
||||
state = state and True
|
||||
elif self.is_not_idle:
|
||||
self.setEnabled(False)
|
||||
return
|
||||
self.setEnabled(state)
|
||||
|
||||
# property getter/setters
|
||||
|
||||
# machine is on status
|
||||
def set_is_on(self, data):
|
||||
self.is_on = data
|
||||
def get_is_on(self):
|
||||
return self.is_on
|
||||
def reset_is_on(self):
|
||||
self.is_on = False
|
||||
is_on_status = Property(bool, get_is_on, set_is_on, reset_is_on)
|
||||
|
||||
# machine is idle status
|
||||
def set_is_idle(self, data):
|
||||
self.is_idle = data
|
||||
if (data and self.is_not_idle):
|
||||
self.is_not_idle = False
|
||||
def get_is_idle(self):
|
||||
return self.is_idle
|
||||
def reset_is_idle(self):
|
||||
self.is_idle = False
|
||||
is_idle_status = Property(bool, get_is_idle, set_is_idle, reset_is_idle)
|
||||
|
||||
# machine is not idle status
|
||||
def set_is_not_idle(self, data):
|
||||
self.is_not_idle = data
|
||||
if (data and self.is_idle):
|
||||
self.is_idle = False
|
||||
def get_is_not_idle(self):
|
||||
return self.is_not_idle
|
||||
def reset_is_not_idle(self):
|
||||
self.is_not_idle = False
|
||||
is_not_idle_status = Property(bool, get_is_not_idle, set_is_not_idle, reset_is_not_idle)
|
||||
|
||||
# machine is homed status
|
||||
def set_is_homed(self, data):
|
||||
self.is_homed = data
|
||||
def get_is_homed(self):
|
||||
return self.is_homed
|
||||
def reset_is_homed(self):
|
||||
self.is_homed = False
|
||||
is_homed_status = Property(bool, get_is_homed, set_is_homed, reset_is_homed)
|
||||
|
||||
class JointEnableWidget(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(JointEnableWidget, self).__init__(parent)
|
||||
self.joint = 1
|
||||
self.axis = 'A'
|
||||
self.joint_mode = False
|
||||
self.axis_mode = False
|
||||
self.if_joint_available = False
|
||||
self.if_axis_available = True
|
||||
|
||||
def _hal_init(self):
|
||||
if self.if_joint_available:
|
||||
if not self.joint in INFO.AVAILABLE_JOINTS:
|
||||
self.setVisible(False)
|
||||
elif self.if_axis_available:
|
||||
if not self.axis.upper() in INFO.AVAILABLE_AXES:
|
||||
self.setVisible(False)
|
||||
if self.joint_mode or self.axis_mode:
|
||||
STATUS.connect('motion-mode-changed', lambda w,data: self.modeChanged(data))
|
||||
|
||||
def modeChanged(self, mode):
|
||||
if mode == STATUS.is_joint_mode():
|
||||
self.setVisible(self.joint_mode)
|
||||
else:
|
||||
self.setVisible(self.axis_mode)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('only_in_axis_mode', 'only_in_joint_mode', )
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i] = False
|
||||
|
||||
def set_joint(self, data):
|
||||
self.joint = data
|
||||
def get_joint(self):
|
||||
return self.joint
|
||||
def reset_joint(self):
|
||||
self.joint = -1
|
||||
|
||||
def set_axis(self, data):
|
||||
if data.upper() in('X','Y','Z','A','B','C','U','V','W'):
|
||||
self.axis = data.upper()
|
||||
def get_axis(self):
|
||||
return self.axis
|
||||
def reset_axis(self):
|
||||
self.axis = 'X'
|
||||
|
||||
def set_joint_mode(self, data):
|
||||
self.joint_mode = data
|
||||
if data:
|
||||
self._toggle_properties('only_in_joint_mode')
|
||||
def get_joint_mode(self):
|
||||
return self.joint_mode
|
||||
def reset_joint_mode(self):
|
||||
self.joint_mode = False
|
||||
|
||||
def set_axis_mode(self, data):
|
||||
self.axis_mode = data
|
||||
if data:
|
||||
self._toggle_properties('only_in_axis_mode')
|
||||
def get_axis_mode(self):
|
||||
return self.axis_mode
|
||||
def reset_axis_mode(self):
|
||||
self.axis_mode = False
|
||||
|
||||
def set_axis_available(self, data):
|
||||
self.if_axis_available = data
|
||||
def get_axis_available(self):
|
||||
return self.if_axis_available
|
||||
def reset_axis_available(self):
|
||||
self.if_axis_available = False
|
||||
|
||||
def set_joint_available(self, data):
|
||||
self.if_joint_available = data
|
||||
def get_joint_available(self):
|
||||
return self.if_joint_available
|
||||
def reset_joint_available(self):
|
||||
self.if_joint_available = False
|
||||
|
||||
joint_number = Property(int, get_joint, set_joint, reset_joint)
|
||||
axis_letter = Property(str, get_axis, set_axis, reset_axis)
|
||||
only_in_joint_mode = Property(bool, get_joint_mode, set_joint_mode, reset_joint_mode)
|
||||
only_in_axis_mode = Property(bool, get_axis_mode, set_axis_mode, reset_axis_mode)
|
||||
only_if_axis_available = Property(bool, get_axis_available, set_axis_available, reset_axis_available)
|
||||
only_if_joint_available = Property(bool, get_joint_available, set_joint_available, reset_joint_available)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
334
lib/python/qtvcp/widgets/detach_tabs.py
Normal file
@@ -0,0 +1,334 @@
|
||||
from qtpy.QtGui import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtWidgets import *
|
||||
|
||||
class DetachTabWidget(QTabWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(DetachTabWidget, self).__init__(parent)
|
||||
|
||||
self.tabBar = self.TabBar(self)
|
||||
self.tabBar.onDetachTabSignal.connect(self.detachTab)
|
||||
self.tabBar.onMoveTabSignal.connect(self.moveTab)
|
||||
|
||||
self.setTabBar(self.tabBar)
|
||||
|
||||
self.btn = QPushButton(self)
|
||||
self.btn.setEnabled(True)
|
||||
#self.btn.setMinimumSize(64, 40)
|
||||
#self.btn.setIconSize(QSize(38, 38))
|
||||
self.btn.setIcon(QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png'))
|
||||
self.btn.clicked.connect(self.invokeDetachTab)
|
||||
self.setCornerWidget(self.btn)
|
||||
|
||||
self._lastgeometry = None
|
||||
|
||||
@Slot(bool)
|
||||
def invokeDetachTab(self, **args):
|
||||
x = self.btn.geometry().x()
|
||||
y = self.btn.geometry().y()
|
||||
self.detachTab(self.currentIndex(), QPoint(x,y))
|
||||
|
||||
##
|
||||
# The default movable functionality of QTabWidget must remain disabled
|
||||
# so as not to conflict with the added features
|
||||
def setMovable(self, movable):
|
||||
pass
|
||||
|
||||
##
|
||||
# Move a tab from one position (index) to another
|
||||
#
|
||||
# @param fromIndex the original index location of the tab
|
||||
# @param toIndex the new index location of the tab
|
||||
@Slot(int, int)
|
||||
def moveTab(self, fromIndex, toIndex):
|
||||
widget = self.widget(fromIndex)
|
||||
icon = self.tabIcon(fromIndex)
|
||||
text = self.tabText(fromIndex)
|
||||
|
||||
self.removeTab(fromIndex)
|
||||
self.insertTab(toIndex, widget, icon, text)
|
||||
self.setCurrentIndex(toIndex)
|
||||
|
||||
|
||||
##
|
||||
# Detach the tab by removing it's contents and placing them in
|
||||
# a DetachedTab dialog
|
||||
#
|
||||
# @param index the index location of the tab to be detached
|
||||
# @param point the screen position for creating the new DetachedTab dialog
|
||||
@Slot(int, QPoint)
|
||||
def detachTab(self, index, point):
|
||||
|
||||
# Get the tab content
|
||||
name = self.tabText(index)
|
||||
icon = self.tabIcon(index)
|
||||
if icon.isNull():
|
||||
icon = self.window().windowIcon()
|
||||
contentWidget = self.widget(index)
|
||||
contentWidgetRect = contentWidget.frameGeometry()
|
||||
|
||||
# Create a new detached tab window
|
||||
detachedTab = self.DetachedTab(contentWidget, self.parentWidget())
|
||||
detachedTab.setWindowModality(Qt.NonModal)
|
||||
detachedTab.setWindowTitle(name + ' - Close to reattach')
|
||||
detachedTab.setWindowIcon(icon)
|
||||
detachedTab.setObjectName(name)
|
||||
if self._lastgeometry is None:
|
||||
detachedTab.setGeometry(contentWidgetRect)
|
||||
detachedTab.move(point)
|
||||
else:
|
||||
detachedTab.setGeometry(self._lastgeometry)
|
||||
|
||||
detachedTab.onCloseSignal.connect(self.attachTab)
|
||||
detachedTab.show()
|
||||
|
||||
##
|
||||
# Re-attach the tab by removing the content from the DetachedTab dialog,
|
||||
# closing it, and placing the content back into the DetachTabWidget
|
||||
#
|
||||
# @param contentWidget the content widget from the DetachedTab dialog
|
||||
# @param name the name of the detached tab
|
||||
# @param icon the window icon for the detached tab
|
||||
@Slot(QWidget, type(''), QIcon, QRect)
|
||||
def attachTab(self, contentWidget, name, icon, geometry):
|
||||
|
||||
self._lastgeometry = geometry
|
||||
|
||||
# Make the content widget a child of this widget
|
||||
contentWidget.setParent(self)
|
||||
|
||||
|
||||
# Create an image from the given icon
|
||||
if not icon.isNull():
|
||||
tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
|
||||
tabIconImage = tabIconPixmap.toImage()
|
||||
else:
|
||||
tabIconImage = None
|
||||
|
||||
|
||||
# Create an image of the main window icon
|
||||
if not icon.isNull():
|
||||
windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
|
||||
windowIconImage = windowIconPixmap.toImage()
|
||||
else:
|
||||
windowIconImage = None
|
||||
|
||||
|
||||
# Determine if the given image and the main window icon are the same.
|
||||
# If they are, then do not add the icon to the tab
|
||||
if tabIconImage == windowIconImage:
|
||||
index = self.addTab(contentWidget, name)
|
||||
else:
|
||||
index = self.addTab(contentWidget, icon, name)
|
||||
|
||||
|
||||
# Make this tab the current tab
|
||||
if index > -1:
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
|
||||
##
|
||||
# When a tab is detached, the contents are placed into this QDialog. The tab
|
||||
# can be re-attached by closing the dialog or by double clicking on its
|
||||
# window frame.
|
||||
class DetachedTab(QDialog):
|
||||
onCloseSignal = Signal(QWidget,type(''), QIcon, QRect)
|
||||
|
||||
def __init__(self, contentWidget, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.contentWidget = contentWidget
|
||||
layout.addWidget(self.contentWidget)
|
||||
self.contentWidget.show()
|
||||
self.setWindowFlags(Qt.Window)
|
||||
|
||||
|
||||
##
|
||||
# Capture a double click event on the dialog's window frame
|
||||
#
|
||||
# @param event an event
|
||||
#
|
||||
# @return true if the event was recognized
|
||||
def event(self, event):
|
||||
|
||||
# If the event type is QEvent.NonClientAreaMouseButtonDblClick then
|
||||
# close the dialog
|
||||
if event.type() == 176:
|
||||
event.accept()
|
||||
self.close()
|
||||
|
||||
return QDialog.event(self, event)
|
||||
|
||||
|
||||
##
|
||||
# If the dialog is closed, emit the onCloseSignal and give the
|
||||
# content widget back to the DetachTabWidget
|
||||
#
|
||||
# @param event a close event
|
||||
def closeEvent(self, event):
|
||||
self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon(),self.geometry())
|
||||
|
||||
|
||||
##
|
||||
# The TabBar class re-implements some of the functionality of the QTabBar widget
|
||||
class TabBar(QTabBar):
|
||||
onDetachTabSignal = Signal(int, QPoint)
|
||||
onMoveTabSignal = Signal(int, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTabBar.__init__(self, parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.setElideMode(Qt.ElideRight)
|
||||
self.setSelectionBehaviorOnRemove(QTabBar.SelectLeftTab)
|
||||
|
||||
self.dragStartPos = QPoint()
|
||||
self.dragDropedPos = QPoint()
|
||||
self.mouseCursor = QCursor()
|
||||
self.dragInitiated = False
|
||||
|
||||
|
||||
##
|
||||
# Send the onDetachTabSignal when a tab is double clicked
|
||||
#
|
||||
# @param event a mouse double click event
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
event.accept()
|
||||
self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
|
||||
|
||||
|
||||
##
|
||||
# Set the starting position for a drag event when the mouse button is pressed
|
||||
#
|
||||
# @param event a mouse press event
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.dragStartPos = event.pos()
|
||||
|
||||
self.dragDropedPos.setX(0)
|
||||
self.dragDropedPos.setY(0)
|
||||
|
||||
self.dragInitiated = False
|
||||
|
||||
QTabBar.mousePressEvent(self, event)
|
||||
|
||||
|
||||
##
|
||||
# Determine if the current movement is a drag. If it is, convert it into a QDrag. If the
|
||||
# drag ends inside the tab bar, emit an onMoveTabSignal. If the drag ends outside the tab
|
||||
# bar, emit an onDetachTabSignal.
|
||||
#
|
||||
# @param event a mouse move event
|
||||
def mouseMoveEvent(self, event):
|
||||
|
||||
# Determine if the current movement is detected as a drag
|
||||
if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() > QApplication.startDragDistance()):
|
||||
self.dragInitiated = True
|
||||
|
||||
# If the current movement is a drag initiated by the left button
|
||||
if (((event.buttons() & Qt.LeftButton)) and self.dragInitiated):
|
||||
|
||||
# Stop the move event
|
||||
finishMoveEvent = QMouseEvent(QEvent.MouseMove, event.pos(), Qt.NoButton, Qt.NoButton, Qt.NoModifier)
|
||||
QTabBar.mouseMoveEvent(self, finishMoveEvent)
|
||||
|
||||
# Convert the move event into a drag
|
||||
drag = QDrag(self)
|
||||
mimeData = QMimeData()
|
||||
mimeData.setData('action', b'application/tab-detach')
|
||||
drag.setMimeData(mimeData)
|
||||
|
||||
#Create the appearance of dragging the tab content
|
||||
pixmap = self.parentWidget().grab()
|
||||
targetPixmap = QPixmap(pixmap.size())
|
||||
targetPixmap.fill(Qt.transparent)
|
||||
painter = QPainter(targetPixmap)
|
||||
painter.setOpacity(0.85)
|
||||
painter.drawPixmap(0, 0, pixmap)
|
||||
painter.end()
|
||||
drag.setPixmap(targetPixmap)
|
||||
|
||||
# Initiate the drag
|
||||
dropAction = drag.exec_(Qt.MoveAction | Qt.CopyAction)
|
||||
|
||||
# If the drag completed outside of the tab bar, detach the tab and move
|
||||
# the content to the current cursor position
|
||||
if dropAction == Qt.IgnoreAction:
|
||||
event.accept()
|
||||
self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
|
||||
|
||||
# Else if the drag completed inside the tab bar, move the selected tab to the new position
|
||||
elif dropAction == Qt.MoveAction:
|
||||
if not self.dragDropedPos.isNull():
|
||||
event.accept()
|
||||
self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
|
||||
else:
|
||||
QTabBar.mouseMoveEvent(self, event)
|
||||
|
||||
|
||||
##
|
||||
# Determine if the drag has entered a tab position from another tab position
|
||||
#
|
||||
# @param event a drag enter event
|
||||
def dragEnterEvent(self, event):
|
||||
mimeData = event.mimeData()
|
||||
formats = mimeData.formats()
|
||||
|
||||
if 'action' in formats and mimeData.data('action') == 'application/tab-detach':
|
||||
event.acceptProposedAction()
|
||||
|
||||
QTabBar.dragMoveEvent(self, event)
|
||||
|
||||
|
||||
##
|
||||
# Get the position of the end of the drag
|
||||
#
|
||||
# @param event a drop event
|
||||
def dropEvent(self, event):
|
||||
self.dragDropedPos = event.pos()
|
||||
QTabBar.dropEvent(self, event)
|
||||
|
||||
class SurfViewer(QMainWindow):
|
||||
def __init__(self, parent=None):
|
||||
super(SurfViewer, self).__init__()
|
||||
self.parent = parent
|
||||
self.centralTabs= DetachTabWidget()
|
||||
self.setCentralWidget(self.centralTabs)
|
||||
# self.setFixedWidth(200)
|
||||
#self.setFixedHeight(200)
|
||||
|
||||
#tab 1
|
||||
self.tab_1 = QWidget()
|
||||
self.centralTabs.addTab(self.tab_1,"Label 1")
|
||||
vbox = QVBoxLayout()
|
||||
Label = QLabel('Tab1')
|
||||
Label.setFixedWidth(180)
|
||||
LineEdit = QLineEdit('Tab1')
|
||||
LineEdit.setFixedWidth(180)
|
||||
vbox.addWidget(Label)
|
||||
vbox.addWidget(LineEdit)
|
||||
vbox.setAlignment(Qt.AlignTop)
|
||||
self.tab_1.setLayout(vbox)
|
||||
|
||||
#tab 2
|
||||
self.tab_2 = QWidget()
|
||||
self.centralTabs.addTab(self.tab_2,"Label 2")
|
||||
vbox = QVBoxLayout()
|
||||
Label = QLabel('Tab2')
|
||||
Label.setFixedWidth(180)
|
||||
LineEdit = QLineEdit('Tab2')
|
||||
LineEdit.setFixedWidth(180)
|
||||
vbox.addWidget(Label)
|
||||
vbox.addWidget(LineEdit)
|
||||
vbox.setAlignment(Qt.AlignTop)
|
||||
self.tab_2.setLayout(vbox)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
app = QApplication(sys.argv)
|
||||
ex = SurfViewer(app)
|
||||
ex.setWindowTitle('window')
|
||||
ex.show()
|
||||
sys.exit(app.exec_( ))
|
||||
|
||||
176
lib/python/qtvcp/widgets/dialogMixin.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from qtpy.QtWidgets import (QApplication, QMessageBox)
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
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
|
||||
|
||||
#########################################
|
||||
# geometry helper functions
|
||||
#########################################
|
||||
|
||||
# This general function parses the geometry string and places
|
||||
# the dialog based on what it finds.
|
||||
# there are directive words allowed.
|
||||
# If there are no letters in the string, it will check the
|
||||
# preference file (if there is one) to see what the last position
|
||||
# was. If all else fails it uses it's natural Designer stated
|
||||
# geometry
|
||||
class GeometryMixin(_HalWidgetBase):
|
||||
def __init__(self, ):
|
||||
super(GeometryMixin, self).__init__()
|
||||
self._geometry_string = 'default'
|
||||
self._default_geometry = None
|
||||
|
||||
def get_default_geometry(self):
|
||||
a,b,c,d = self._default_geometry
|
||||
return '%s %s %s %s'% (a,b,c,d)
|
||||
|
||||
def set_default_geometry(self):
|
||||
geom = self.frameGeometry()
|
||||
geom.moveCenter(QApplication.primaryScreen().availableGeometry().center())
|
||||
self.setGeometry(geom)
|
||||
x = self.geometry().x()
|
||||
y = self.geometry().y()
|
||||
w = 300 #w = self.geometry().width()
|
||||
h = 150 #h = self.geometry().height()
|
||||
self._default_geometry=[x,y,w,h]
|
||||
return x,y,w,h
|
||||
|
||||
# only valid is dialog has been shown
|
||||
def get_current_geometry(self):
|
||||
x = self.geometry().x()
|
||||
y = self.geometry().y()
|
||||
w = self.geometry().width()
|
||||
h = self.geometry().height()
|
||||
return '%s %s %s %s'% (x,y,w,h)
|
||||
|
||||
def read_preference_geometry(self,name):
|
||||
self._geoName = name
|
||||
if self.PREFS_:
|
||||
self._geometry_string = self.PREFS_.getpref(name,
|
||||
self._geometry_string,
|
||||
str, 'DIALOG_GEOMETRY')
|
||||
|
||||
def set_geometry(self):
|
||||
#print(self.objectName(),'set geo',self._geometry_string)
|
||||
try:
|
||||
if self._geometry_string.replace(' ','').isdigit() and self.PREFS_:
|
||||
# If there is a preference file object use it to load the geometry
|
||||
self._geometry_string = self.PREFS_.getpref(self._geoName, '', str, 'DIALOG_GEOMETRY')
|
||||
|
||||
# use the previously calculated default.
|
||||
if self._geometry_string in('default',''):
|
||||
x,y,w,h = self._default_geometry
|
||||
self.setGeometry(x,y,w,h)
|
||||
if not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
|
||||
# center of desktop
|
||||
# add 'always' or the user can reset the dialog
|
||||
elif 'center' in self._geometry_string.lower():
|
||||
geom = self.frameGeometry()
|
||||
geom.moveCenter(QApplication.primaryScreen().availableGeometry().center())
|
||||
self.setGeometry(geom)
|
||||
if not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
|
||||
# bottom left of desktop
|
||||
# add 'always' or the user can reset the dialog
|
||||
elif 'bottomleft' in self._geometry_string.lower():
|
||||
# move to bottom left of parent
|
||||
ph = QApplication.primaryScreen().geometry().height()
|
||||
px = QApplication.primaryScreen().geometry().x()
|
||||
py = QApplication.primaryScreen().geometry().y()
|
||||
dw = self.geometry().width()
|
||||
dh = self.geometry().height()
|
||||
self.setGeometry(px, py+ph-dh, dw, dh)
|
||||
if not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
|
||||
# to be always on (relative to) parent but as assigned size
|
||||
# ie: Dialog-geometry = onwindow 100 100 280 118
|
||||
# add 'always' or the user can reset the dialog
|
||||
elif 'onwindow' in self._geometry_string.lower():
|
||||
# move relative to parent position
|
||||
px = self.QTVCP_INSTANCE_.geometry().x()
|
||||
py = self.QTVCP_INSTANCE_.geometry().y()
|
||||
# remove everything except digits and spaces
|
||||
temp=''
|
||||
for x in self._geometry_string:
|
||||
if (x.isdigit() or x == ' '):
|
||||
temp = temp+x
|
||||
# remove lead and trailing spaces and then split on spaces
|
||||
temp = temp.strip(' ').split(' ')
|
||||
self.setGeometry(px+int(temp[0]), py+int(temp[1]), int(temp[2]), int(temp[3]))
|
||||
if not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
|
||||
# half the main window height/width
|
||||
# add 'always' or the user can reset the dialog
|
||||
elif 'half' in self._geometry_string.lower():
|
||||
h = int(self.QTVCP_INSTANCE_.geometry().height() /2)
|
||||
w = int(self.QTVCP_INSTANCE_.geometry().width() /2)
|
||||
|
||||
x = int(self.geometry().x())
|
||||
y = int(self.geometry().y())
|
||||
self.setGeometry( x,y,w,h)
|
||||
|
||||
if not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
|
||||
else:
|
||||
# assuming geometry is actual size/position
|
||||
temp = self._geometry_string.split(' ')
|
||||
self.setGeometry(int(temp[0]), int(temp[1]), int(temp[2]), int(temp[3]))
|
||||
LOG.debug('Setting {} dialog geometry from {} from prefs.'.format( self._geoName,temp))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
try:
|
||||
LOG.error('Calculating geometry of {} widget using: {}. Will use default placement.'.format(self.HAL_NAME_, self._geometry_string))
|
||||
except AttributeError as f:
|
||||
print(f)
|
||||
LOG.debug('Dialog geometry python error: {}'.format(e))
|
||||
x = self.geometry().x()
|
||||
y = self.geometry().y()
|
||||
self.setGeometry( x,y,300,150)
|
||||
self._geometry_string = 'default'
|
||||
|
||||
def record_geometry(self):
|
||||
# message box are difficult
|
||||
if isinstance(self,QMessageBox):
|
||||
if self._geometry_string.replace(' ','').isdigit():
|
||||
if self.PREFS_ :
|
||||
geometry_string = self.PREFS_.getpref(self._geoName, '', str, 'DIALOG_GEOMETRY')
|
||||
else:
|
||||
geometry_string = ''
|
||||
temp = self._geometry_string.split(' ')
|
||||
#print('record:',temp,self.geometry())
|
||||
x = self.geometry().x()
|
||||
y = self.geometry().y()
|
||||
w = int(temp[2])
|
||||
h = self.geometry().height()
|
||||
geo = '%s %s %s %s'% (x,y,w,h)
|
||||
if self.PREFS_ :
|
||||
self.PREFS_.putpref(self._geoName, geo, str, 'DIALOG_GEOMETRY')
|
||||
return
|
||||
try:
|
||||
if self.PREFS_ :
|
||||
temp = self._geometry_string.replace(' ','')
|
||||
temp = temp.strip('-')
|
||||
if temp in('','default') or temp.isdigit():
|
||||
LOG.debug('Saving {} data from widget {} to file.'.format( self._geoName,self.HAL_NAME_))
|
||||
x = self.geometry().x()
|
||||
y = self.geometry().y()
|
||||
w = self.geometry().width()
|
||||
h = self.geometry().height()
|
||||
geo = '%s %s %s %s'% (x,y,w,h)
|
||||
self.PREFS_.putpref(self._geoName, geo, str, 'DIALOG_GEOMETRY')
|
||||
elif not 'always' in self._geometry_string.lower():
|
||||
self._geometry_string = self.get_current_geometry()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
|
||||
2284
lib/python/qtvcp/widgets/dialog_widget.py
Normal file
512
lib/python/qtvcp/widgets/dro_widget.py
Normal file
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtVcp Widget - DRO label widget
|
||||
# This widgets displays linuxcnc axis position information.
|
||||
#
|
||||
# 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 linuxcnc
|
||||
|
||||
from qtpy import QtCore
|
||||
from qtpy.QtWidgets import QMenu, QAction
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.simple_widgets import ScaledLabel
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Action
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# INFO holds INI file details
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class DROLabel(ScaledLabel, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(DROLabel, self).__init__(parent)
|
||||
self._mode = False
|
||||
self._joint_type = 1
|
||||
self.diameter = False
|
||||
self.reference_type = 0
|
||||
# axis index of 9 axis
|
||||
self.axis_index = 0
|
||||
# linuxcnc joint number
|
||||
self._jointNum = 0
|
||||
self.display_units_mm = 0
|
||||
self.metric_text_template = '%10.3f'
|
||||
self.imperial_text_template = '%9.4f'
|
||||
self.angular_text_template = '%9.2f'
|
||||
self.setText('--------------')
|
||||
self.allow_reference_change_requests = True
|
||||
self.follow_m7m8_mode = True
|
||||
self.force_diameter = False
|
||||
self.force_radius = False
|
||||
self._scale = 1
|
||||
self._user = 0
|
||||
self._text =' -000.0000'
|
||||
|
||||
# menu stuff
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self._showGoto = False
|
||||
self._showZero = False
|
||||
self._showSet = False
|
||||
self._showLast = False
|
||||
self._showDivide = False
|
||||
|
||||
# for stylesheet reading
|
||||
self._isHomed = False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
menu = QMenu(self)
|
||||
state = (STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
|
||||
if self._showZero:
|
||||
self.zeroButton = QAction(QIcon('exit24.png'), 'Zero', self)
|
||||
self.zeroButton.triggered.connect(self.Zero)
|
||||
self.zeroButton.setEnabled(state)
|
||||
menu.addAction(self.zeroButton)
|
||||
if self._showSet:
|
||||
self.setButton = QAction(QIcon('exit24.png'), 'Set', self)
|
||||
self.setButton.triggered.connect(self.SetOrigin)
|
||||
self.setButton.setEnabled(state)
|
||||
menu.addAction(self.setButton)
|
||||
if self._showDivide:
|
||||
self.divideButton = QAction(QIcon('exit24.png'), 'Divide By 2', self)
|
||||
self.divideButton.triggered.connect(self.Divide)
|
||||
self.divideButton.setEnabled(state)
|
||||
menu.addAction(self.divideButton)
|
||||
if self._showLast:
|
||||
self.lastButton = QAction(QIcon('exit24.png'), 'Set To Last', self)
|
||||
self.lastButton.triggered.connect(self.Last)
|
||||
self.lastButton.setEnabled(state)
|
||||
menu.addAction(self.lastButton)
|
||||
if self._showGoto:
|
||||
text = 'Go To G53 Origin in {}'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index))
|
||||
self.goToG53Button = QAction(QIcon('exit24.png'), text, self)
|
||||
self.goToG53Button.triggered.connect(self.goToG53)
|
||||
self.goToG53Button.setEnabled(state)
|
||||
menu.addAction(self.goToG53Button)
|
||||
text = 'Go To G5x Origin in {}'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index))
|
||||
self.goToG5xButton = QAction(QIcon('exit24.png'), text, self)
|
||||
self.goToG5xButton.triggered.connect(self.goToG5x)
|
||||
self.goToG5xButton.setEnabled(state)
|
||||
menu.addAction(self.goToG5xButton)
|
||||
|
||||
menu.popup(event.globalPos())
|
||||
|
||||
def _hal_init(self):
|
||||
super(DROLabel, self)._hal_init()
|
||||
STATUS.connect('homed', lambda w,d: self._home_status_polish(int(d), True))
|
||||
STATUS.connect('unhomed', lambda w,d: self._home_status_polish(int(d), False))
|
||||
# get position update from STATUS every 100 ms
|
||||
if self.axis_index == 10:
|
||||
STATUS.connect('current-z-rotation', self.update_rotation)
|
||||
else:
|
||||
self._jointNum = INFO.GET_JOINT_NUM_FROM_AXIS_INDEX.get(self.axis_index)
|
||||
if self._jointNum is None:
|
||||
LOG.debug('axis number {} not found in available-axis to joint conversion dict {} of widget: {}'.format(self.axis_index, INFO.GET_JOINT_NUM_FROM_AXIS_INDEX, self.objectName()))
|
||||
self._jointNum = 0
|
||||
|
||||
STATUS.connect('motion-mode-changed',self.motion_mode)
|
||||
STATUS.connect('current-position', self.update)
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
if self.follow_m7m8_mode:
|
||||
STATUS.connect('diameter-mode', self._switch_modes)
|
||||
if self.allow_reference_change_requests:
|
||||
STATUS.connect('dro-reference-change-request', self._status_reference_change)
|
||||
|
||||
try:
|
||||
self._joint_type = INFO.JOINT_TYPE_INT[self._jointNum]
|
||||
except:
|
||||
self._joint_type = 1
|
||||
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
elif self.display_units_mm:
|
||||
self._current_text_template = self.metric_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
# update ishomed property
|
||||
# polish widget so stylesheet sees the property change
|
||||
# some stylesheets color the text on home/unhome
|
||||
def _home_status_polish(self, d, state):
|
||||
if d == self._jointNum or (self.axis_index==10 and d==1):
|
||||
self.setProperty('isHomed', state)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
def motion_mode(self, w, mode):
|
||||
if mode == linuxcnc.TRAJ_MODE_COORD:
|
||||
pass
|
||||
# Joint mode
|
||||
elif mode == linuxcnc.TRAJ_MODE_FREE:
|
||||
self._mode = False
|
||||
# axis
|
||||
elif mode == linuxcnc.TRAJ_MODE_TELEOP:
|
||||
self._mode = True
|
||||
|
||||
@QtCore.Slot(int)
|
||||
@QtCore.Slot(float)
|
||||
def update_user(self, data):
|
||||
self._user = data
|
||||
|
||||
def update_rotation(self, widget, rotation):
|
||||
degtmpl = lambda s: self.angular_text_template % s
|
||||
self.setText(degtmpl(rotation))
|
||||
|
||||
def update(self, widget, absolute, relative, dtg, joint):
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
absolute = INFO.convert_units_9(absolute)
|
||||
relative = INFO.convert_units_9(relative)
|
||||
dtg = INFO.convert_units_9(dtg)
|
||||
|
||||
tmpl = lambda s: self._current_text_template % s
|
||||
|
||||
try:
|
||||
if self.reference_type == 0:
|
||||
if not self._mode and STATUS.stat.kinematics_type != linuxcnc.KINEMATICS_IDENTITY:
|
||||
self.setText(tmpl(joint[self._jointNum]))
|
||||
else:
|
||||
self.setText(tmpl(absolute[self.axis_index]*self._scale))
|
||||
elif self.reference_type == 1:
|
||||
self.setText(tmpl(relative[self.axis_index]*self._scale))
|
||||
elif self.reference_type == 2:
|
||||
self.setText(tmpl(dtg[self.axis_index]*self._scale))
|
||||
|
||||
elif self.reference_type == 10:
|
||||
self.setText(tmpl(self._user*self._scale))
|
||||
except:
|
||||
pass
|
||||
|
||||
def _status_reference_change(self,w ,value):
|
||||
self.reference_type = value
|
||||
|
||||
def _switch_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
self.update_units()
|
||||
|
||||
def update_units(self):
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
elif self.display_units_mm:
|
||||
self._current_text_template = self.metric_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
def _switch_modes(self, w, mode):
|
||||
self.diameter = mode
|
||||
# only joint 0 (X) can use diameter mode
|
||||
if mode and self.axis_index == 0:
|
||||
self._scale = 2.0
|
||||
else:
|
||||
self._scale = 1
|
||||
|
||||
def set_to_inch(self):
|
||||
self.display_units_mm = 0
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
def set_to_mm(self):
|
||||
self.display_units_mm = 1
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
else:
|
||||
self._current_text_template = self.metric_text_template
|
||||
|
||||
def set_to_diameter(self):
|
||||
self.diameter = True
|
||||
if self.axis_index == 0:
|
||||
self._scale = 2.0
|
||||
|
||||
def set_to_radius(self):
|
||||
self.diameter = False
|
||||
self._scale = 1.0
|
||||
|
||||
# index = index of 9 axis
|
||||
def set_joint_type(self, index):
|
||||
# convert to linxcnc joint number
|
||||
self._jointNum = INFO.GET_JOINT_NUM_FROM_AXIS_INDEX.get(index)
|
||||
if self._jointNum is None:
|
||||
self._joint_type = 1
|
||||
else:
|
||||
self._joint_type = INFO.JOINT_TYPE_INT[self._jointNum]
|
||||
self. update_units()
|
||||
|
||||
#######################
|
||||
# menu option code
|
||||
#######################
|
||||
|
||||
def Zero(self):
|
||||
pos = self.get_current_position()
|
||||
if not pos is None:
|
||||
axis = "XYZABCUVW"[self.axis_index]
|
||||
ACTION.SET_AXIS_ORIGIN(axis, 0)
|
||||
STATUS.emit('update-machine-log', 'Zeroed Axis %s' % axis, 'TIME,SUCCESS')
|
||||
|
||||
def SetOrigin(self):
|
||||
pos = self.get_current_position()
|
||||
if not pos is None:
|
||||
axis = "XYZABCUVW"[self.axis_index]
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,
|
||||
'CURRENT':pos,
|
||||
'TITLE':'Set %s Origin'% axis,
|
||||
'GEONAME':'DROWidgetDialog_{}'.format(self.dialog_code),
|
||||
'AXIS':axis}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
axis = message.get('AXIS')
|
||||
ACTION.SET_AXIS_ORIGIN(axis, num)
|
||||
STATUS.emit('update-machine-log', 'Set Origin of Axis %s to %f' %(axis, num), 'TIME,SUCCESS')
|
||||
|
||||
def Divide(self):
|
||||
pos = self.get_current_position()
|
||||
if not pos is None:
|
||||
axis = "XYZABCUVW"[self.axis_index]
|
||||
try:
|
||||
x = pos/2.0
|
||||
ACTION.SET_AXIS_ORIGIN(axis, x)
|
||||
text = 'Divided Axis %s in Half - %f'% (axis, x)
|
||||
STATUS.emit('update-machine-log', text, 'TIME,SUCCESS')
|
||||
except ZeroDivisionError:
|
||||
pass
|
||||
|
||||
def Last(self):
|
||||
pos = self.get_current_position()
|
||||
if not pos is None:
|
||||
axis = "XYZABCUVW"[self.axis_index]
|
||||
last = ACTION.GET_LAST_RECORDED_ORIGIN(axis)
|
||||
ACTION.SET_AXIS_ORIGIN(axis, last)
|
||||
text = 'Reset Axis %s from %f to Last Value: %f' %(axis, pos, last)
|
||||
STATUS.emit('update-machine-log', text, 'TIME,SUCCESS')
|
||||
|
||||
def get_current_position(self):
|
||||
if STATUS.is_joint_mode():
|
||||
return None
|
||||
p,r,d = STATUS.get_position()
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
r = INFO.convert_units_9(r)
|
||||
return r[self.axis_index]
|
||||
|
||||
def goToG53(self):
|
||||
ACTION.CALL_MDI('G53 G0 {}0'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index)))
|
||||
def goToG5x(self):
|
||||
ACTION.CALL_MDI('G0 {}0'.format(INFO.GET_NAME_FROM_JOINT.get(self.axis_index)))
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('always_display_diameter','always_display_radius',
|
||||
'display_as_per_m7m8')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i] = False
|
||||
|
||||
# property getter/setters
|
||||
def set_force_diameter(self, data):
|
||||
self.force_diameter = data
|
||||
if data:
|
||||
self.set_to_diameter()
|
||||
self._toggle_properties('always_display_diameter')
|
||||
def get_force_diameter(self):
|
||||
return self.force_diameter
|
||||
def reset_force_diameter(self):
|
||||
self.force_diameter = False
|
||||
always_display_diameter = QtCore.Property(bool, get_force_diameter, set_force_diameter, reset_force_diameter)
|
||||
|
||||
def set_force_radius(self, data):
|
||||
self.force_radius = data
|
||||
if data:
|
||||
self.set_to_radius()
|
||||
self._toggle_properties('always_display_radius')
|
||||
def get_force_radius(self):
|
||||
return self.force_radius
|
||||
def reset_force_radius(self):
|
||||
self.force_radius = False
|
||||
always_display_radius = QtCore.Property(bool, get_force_radius, set_force_radius, reset_force_radius)
|
||||
|
||||
def set_follow_m7m8_mode(self, data):
|
||||
self.follow_m7m8_mode = data
|
||||
if data:
|
||||
self._toggle_properties('display_as_per_m7m8')
|
||||
def get_follow_m7m8_mode(self):
|
||||
return self.follow_m7m8_mode
|
||||
def reset_follow_m7m8_mode(self):
|
||||
self.follow_m7m8_mode = True
|
||||
display_as_per_m7m8 = QtCore.Property(bool, get_follow_m7m8_mode, set_follow_m7m8_mode, reset_follow_m7m8_mode)
|
||||
|
||||
def set_follow_reference(self, data):
|
||||
self.allow_reference_change_requests = data
|
||||
def get_follow_reference(self):
|
||||
return self.allow_reference_change_requests
|
||||
def reset_follow_reference(self):
|
||||
self.allow_reference_change_requests = True
|
||||
follow_reference_changes = QtCore.Property(bool, get_follow_reference, set_follow_reference, reset_follow_reference)
|
||||
|
||||
# JOINT Number TODO this is actually joint index of the 9 axes
|
||||
# rather then linuxcnc's idea of joint number
|
||||
def setjoint_number(self, data):
|
||||
if data >10: data = 10
|
||||
if data < 0: data = 0
|
||||
self.axis_index = data
|
||||
if data < 10:
|
||||
self.set_joint_type(data)
|
||||
def getjoint_number(self):
|
||||
return self.axis_index
|
||||
def resetjoint_number(self):
|
||||
self.axis_index = 0
|
||||
Qjoint_number = QtCore.Property(int, getjoint_number, setjoint_number, resetjoint_number)
|
||||
|
||||
# user system Number
|
||||
def setreference_type(self, data):
|
||||
self.reference_type = data
|
||||
def getreference_type(self):
|
||||
return self.reference_type
|
||||
def resetreference_type(self):
|
||||
self.reference_type = 0
|
||||
Qreference_type = QtCore.Property(int, getreference_type, setreference_type, resetreference_type)
|
||||
|
||||
def setmetrictemplate(self, data):
|
||||
self.metric_text_template = data
|
||||
self.update_units()
|
||||
def getmetrictemplate(self):
|
||||
return self.metric_text_template
|
||||
def resetmetrictemplate(self):
|
||||
self.metric_text_template = '%10.3f'
|
||||
metric_template = QtCore.Property(str, getmetrictemplate, setmetrictemplate, resetmetrictemplate)
|
||||
|
||||
def setimperialtexttemplate(self, data):
|
||||
self.imperial_text_template = data
|
||||
self.update_units()
|
||||
def getimperialtexttemplate(self):
|
||||
return self.imperial_text_template
|
||||
def resetimperialtexttemplate(self):
|
||||
self.imperial_text_template = '%9.4f'
|
||||
imperial_template = QtCore.Property(str, getimperialtexttemplate, setimperialtexttemplate, resetimperialtexttemplate)
|
||||
|
||||
def setangulartexttemplate(self, data):
|
||||
self.angular_text_template = data
|
||||
self.update_units()
|
||||
def getangulartexttemplate(self):
|
||||
return self.angular_text_template
|
||||
def resetangulartexttemplate(self):
|
||||
self.angular_text_template = '%9.2f'
|
||||
angular_template = QtCore.Property(str, getangulartexttemplate, setangulartexttemplate, resetangulartexttemplate)
|
||||
|
||||
def setisHomed(self, data):
|
||||
self._isHomed = data
|
||||
def getisHomed(self):
|
||||
return self._isHomed
|
||||
|
||||
isHomed = QtCore.Property(bool, getisHomed, setisHomed)
|
||||
|
||||
####################
|
||||
## menu properties
|
||||
####################
|
||||
|
||||
def set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialogName = QtCore.Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_showSet(self, data):
|
||||
self._showSet = data
|
||||
def get_showSet(self):
|
||||
return self._showSet
|
||||
def reset_showSet(self):
|
||||
self._showSet = False
|
||||
showSetOrigin = QtCore.Property(bool, get_showSet, set_showSet, reset_showSet)
|
||||
|
||||
def set_showZero(self, data):
|
||||
self._showZero = data
|
||||
def get_showZero(self):
|
||||
return self._showZero
|
||||
def reset_showZero(self):
|
||||
self._showZero = False
|
||||
showZeroOrigin = QtCore.Property(bool, get_showZero, set_showZero, reset_showZero)
|
||||
|
||||
def set_showGoto(self, data):
|
||||
self._showGoto = data
|
||||
def get_showGoto(self):
|
||||
return self._showGoto
|
||||
def reset_showGoto(self):
|
||||
self._showGoto = False
|
||||
showGotoOrigin = QtCore.Property(bool, get_showGoto, set_showGoto, reset_showGoto)
|
||||
|
||||
def set_showLast(self, data):
|
||||
self._showLast = data
|
||||
def get_showLast(self):
|
||||
return self._showLast
|
||||
def reset_showLast(self):
|
||||
self._showLast = False
|
||||
showLast = QtCore.Property(bool, get_showLast, set_showLast, reset_showLast)
|
||||
|
||||
def set_showDivide(self, data):
|
||||
self._showDivide = data
|
||||
def get_showDivide(self):
|
||||
return self._showDivide
|
||||
def reset_showDivide(self):
|
||||
self._showDivide = False
|
||||
showDivide = QtCore.Property(bool, get_showDivide, set_showDivide, reset_showDivide)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = DROLabel()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
285
lib/python/qtvcp/widgets/entry_widget.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
# QTVcp Widget
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# base code Found on Code Cookbook
|
||||
#
|
||||
# This widget pops up an onscreen keyboard for entries
|
||||
# Used in the Macro and MDI line widget
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
#from decimal import Decimal
|
||||
|
||||
# applicationle widgets
|
||||
SIP_WIDGETS = [QtWidgets.QLineEdit]
|
||||
|
||||
|
||||
class MyFlatPushButton(QtWidgets.QPushButton):
|
||||
def __init__(self, caption, min_size=(50, 50)):
|
||||
self.MIN_SIZE = min_size
|
||||
super(MyFlatPushButton, self).__init__(caption)
|
||||
self.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(self.MIN_SIZE[0], self.MIN_SIZE[1])
|
||||
|
||||
class SoftInputWidget(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None, keyboard_type='default'):
|
||||
super(SoftInputWidget, self).__init__(parent)
|
||||
self.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint |
|
||||
QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.INPUT_WIDGET = None
|
||||
self.PARENT_OBJECT = parent
|
||||
self.signalMapper = QtCore.QSignalMapper(self)
|
||||
|
||||
self.NO_ORD_KEY_LIST = list()
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Left)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Up)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Right)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Down)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Backspace)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Enter)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Tab)
|
||||
self.NO_ORD_KEY_LIST.append(QtCore.Qt.Key_Escape)
|
||||
|
||||
self.do_layout(keyboard_type)
|
||||
|
||||
mapped = getattr(self.signalMapper, 'mappedInt', None) or self.signalMapper.mapped[int]
|
||||
mapped.connect(self.buttonClicked)
|
||||
|
||||
def do_layout(self, keyboard_type='default'):
|
||||
"""
|
||||
@param keyboard_type:
|
||||
@return:
|
||||
"""
|
||||
gl = QtWidgets.QVBoxLayout()
|
||||
self.setFont(self.PARENT_OBJECT.font())
|
||||
number_widget_list = []
|
||||
sym_list = list(range(0, 10))
|
||||
for sym in sym_list:
|
||||
button = MyFlatPushButton(str(sym))
|
||||
button.KEY_CHAR = ord(str(sym))
|
||||
number_widget_list.append(button)
|
||||
|
||||
# alphabets
|
||||
alpha_widget_list = []
|
||||
sym_list = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P',
|
||||
'new_row',
|
||||
'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
|
||||
'new_row',
|
||||
'Z', 'X', 'C', 'V', 'B', 'N', 'M']
|
||||
for sym in sym_list:
|
||||
if sym == 'new_row':
|
||||
alpha_widget_list.append('new_row')
|
||||
else:
|
||||
button = MyFlatPushButton(sym)
|
||||
button.KEY_CHAR = ord(sym)
|
||||
alpha_widget_list.append(button)
|
||||
|
||||
button = MyFlatPushButton('.')
|
||||
button.KEY_CHAR = ord('.')
|
||||
alpha_widget_list.append(button)
|
||||
button = MyFlatPushButton('-')
|
||||
button.KEY_CHAR = ord('-')
|
||||
alpha_widget_list.append(button)
|
||||
|
||||
control_widget_list = []
|
||||
|
||||
button = MyFlatPushButton('LINE\nUP')
|
||||
button.setToolTip('Cursor Up')
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Up
|
||||
control_widget_list.append(button)
|
||||
|
||||
button = MyFlatPushButton('LINE\nDOWN')
|
||||
button.setToolTip('Cursor Down')
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Down
|
||||
control_widget_list.append(button)
|
||||
|
||||
# space
|
||||
button = MyFlatPushButton('SPACE', min_size=(160, 50))
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Space
|
||||
control_widget_list.append(button)
|
||||
|
||||
# back space
|
||||
button = MyFlatPushButton('BACK')
|
||||
button.setToolTip('Backspace')
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Backspace
|
||||
control_widget_list.append(button)
|
||||
|
||||
# close
|
||||
button = MyFlatPushButton('CLOSE')
|
||||
button.setToolTip('Close Keyboard')
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Escape
|
||||
control_widget_list.append(button)
|
||||
|
||||
# enter
|
||||
button = MyFlatPushButton('ENTER', min_size=(105, 50))
|
||||
button.setToolTip('Enter Command')
|
||||
button.KEY_CHAR = QtCore.Qt.Key_Enter
|
||||
control_widget_list.append(button)
|
||||
|
||||
MAX_COL = 10
|
||||
col = 0
|
||||
tlist = list()
|
||||
if keyboard_type == 'numeric':
|
||||
widget_list = number_widget_list
|
||||
elif keyboard_type == 'alpha':
|
||||
widget_list = alpha_widget_list
|
||||
else:
|
||||
widget_list = list()
|
||||
widget_list.extend(number_widget_list)
|
||||
widget_list.append('new_row')
|
||||
widget_list.extend(alpha_widget_list)
|
||||
|
||||
widget_list.append('new_row')
|
||||
widget_list.extend(control_widget_list)
|
||||
|
||||
for widget in widget_list:
|
||||
if widget == 'new_row':
|
||||
col = MAX_COL
|
||||
else:
|
||||
tlist.append(widget)
|
||||
widget.clicked.connect(self.signalMapper.map)
|
||||
self.signalMapper.setMapping(widget, widget.KEY_CHAR)
|
||||
|
||||
if col == MAX_COL:
|
||||
col = 0
|
||||
v = QtWidgets.QHBoxLayout()
|
||||
v.addStretch()
|
||||
v.setSpacing(5)
|
||||
list(map(v.addWidget, tlist))
|
||||
v.addStretch()
|
||||
gl.addLayout(v)
|
||||
tlist = []
|
||||
else:
|
||||
col += 1
|
||||
|
||||
v = QtWidgets.QHBoxLayout()
|
||||
v.setSpacing(5)
|
||||
v.addStretch()
|
||||
list(map(v.addWidget, tlist))
|
||||
v.addStretch()
|
||||
gl.addLayout(v)
|
||||
gl.setContentsMargins(4, 4, 4, 4)
|
||||
gl.setSpacing(4)
|
||||
gl.setSizeConstraint(gl.SetFixedSize)
|
||||
|
||||
self.setLayout(gl)
|
||||
|
||||
def reject(self):
|
||||
self.buttonClicked(QtCore.Qt.Key_Escape)
|
||||
|
||||
def buttonClicked(self, char_ord):
|
||||
w = self.INPUT_WIDGET
|
||||
if char_ord in self.NO_ORD_KEY_LIST:
|
||||
keyPress = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, char_ord, QtCore.Qt.NoModifier, '')
|
||||
else:
|
||||
keyPress = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, char_ord, QtCore.Qt.NoModifier, chr(char_ord))
|
||||
# hide on enter or esc button click
|
||||
if char_ord == QtCore.Qt.Key_Escape:
|
||||
self.hide()
|
||||
else:
|
||||
# send keypress event to widget
|
||||
QtWidgets.QApplication.sendEvent(w, keyPress)
|
||||
if char_ord == QtCore.Qt.Key_Enter:
|
||||
self.hide()
|
||||
|
||||
# line edit returnPressed event is triggering twice for press and release both
|
||||
# that is why do not send release event for special key
|
||||
if char_ord not in self.NO_ORD_KEY_LIST:
|
||||
keyRelease = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, char_ord, QtCore.Qt.NoModifier, '')
|
||||
QtWidgets.QApplication.sendEvent(w, keyRelease)
|
||||
|
||||
def show_input_panel(self, widget):
|
||||
self.INPUT_WIDGET = widget
|
||||
self.show()
|
||||
self.update_panel_position()
|
||||
self.setFocus()
|
||||
self.raise_()
|
||||
|
||||
def update_panel_position(self):
|
||||
widget = self.INPUT_WIDGET
|
||||
if not widget: return
|
||||
|
||||
widget_rect = widget.rect()
|
||||
widget_bottom = widget.mapToGlobal(QtCore.QPoint(widget.frameGeometry().x(),
|
||||
widget.frameGeometry().y())).y()
|
||||
screen_height = QtWidgets.QApplication.primaryScreen().availableGeometry().height()
|
||||
input_panel_height = self.geometry().height() + 5
|
||||
|
||||
if (screen_height - widget_bottom) > input_panel_height:
|
||||
# display input panel at bottom of widget
|
||||
panelPos = QtCore.QPoint(widget_rect.left(), widget_rect.bottom() + 2)
|
||||
else:
|
||||
# display input panel at top of widget
|
||||
panelPos = QtCore.QPoint(widget_rect.left(), widget_rect.top() - input_panel_height)
|
||||
|
||||
panelPos = widget.mapToGlobal(panelPos)
|
||||
self.move(panelPos)
|
||||
|
||||
|
||||
# a widget that calls our keyboard dialog
|
||||
class TouchInterface(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(TouchInterface, self).__init__(parent)
|
||||
self._PARENT_WIDGET = parent
|
||||
self._input_panel_alpha = SoftInputWidget(parent, 'alpha')
|
||||
self._input_panel_numeric = SoftInputWidget(parent, 'numeric')
|
||||
self._input_panel_full = SoftInputWidget(parent, 'default')
|
||||
self.keyboard_enable = True
|
||||
self.keyboard_type = 'numeric'
|
||||
|
||||
def childEvent(self, event):
|
||||
if event.type() == QtCore.QEvent.ChildAdded:
|
||||
if isinstance(event.child(), *SIP_WIDGETS):
|
||||
event.child().installEventFilter(self)
|
||||
|
||||
def eventFilter(self, widget, event):
|
||||
try:
|
||||
if self._PARENT_WIDGET.focusWidget() == widget and event.type() == QtCore.QEvent.MouseButtonPress:
|
||||
if self.keyboard_enable is False:
|
||||
return False
|
||||
self.callDialog(widget, self.keyboard_type.lower())
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
# can be class patched to call other entries - like qtvcp dialogs
|
||||
def callDialog(self, widget, ktype):
|
||||
if ktype == 'alpha':
|
||||
self._input_panel_alpha.show_input_panel(widget)
|
||||
elif ktype == 'numeric':
|
||||
self._input_panel_numeric.show_input_panel(widget)
|
||||
else:
|
||||
self._input_panel_full.show_input_panel(widget)
|
||||
|
||||
## testing ##
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from qtpy.QtWidgets import QWidget
|
||||
app = QtWidgets.QApplication([])
|
||||
w = QWidget()
|
||||
w.setGeometry(100, 100, 200, 100)
|
||||
w.setWindowTitle('Entry Widget')
|
||||
line = QtWidgets.QLineEdit()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(line)
|
||||
test = TouchInterface(line)
|
||||
# test.callDialog(line, 'numeric')
|
||||
# test.callDialog(line, 'alpha')
|
||||
test.callDialog(line, 'default')
|
||||
w.setLayout(layout)
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
169
lib/python/qtvcp/widgets/fake_status.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2020 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# This program is used to fake the status of linuxcnc in python for the graphics display
|
||||
# in qtvcp. Now in designer it shows a fake display of an xyz machine.
|
||||
# you could probably extends this to update some attributes and fake tool movement on a demo display
|
||||
#
|
||||
# most entries are default, a few, for axes and joints, to make an XYZ machine
|
||||
|
||||
class fakeStatus():
|
||||
def __init__(self):
|
||||
|
||||
self.acceleration = 0.0
|
||||
self.active_queue = 0
|
||||
self.actual_position = (3.85175736e-09, 3.85175747e-09, 2.95467673e-09, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.adaptive_feed_enabled = False
|
||||
self.ain = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.angular_units = 0 # setting this to 1 breaks nurbs-editor program loading !
|
||||
self.aout = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.axes = 3
|
||||
self.axis = ({'min_position_limit': -40.0, 'velocity': 0.0, 'max_position_limit': 40.0}, {'min_position_limit': -40.0, 'velocity': 0.0, 'max_position_limit': 40.0}, {'min_position_limit': -8.0, 'velocity': 0.0, 'max_position_limit': 0.12}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0}, {'min_position_limit': 0.0, 'velocity': 0.0, 'max_position_limit': 0.0})
|
||||
self.axis_mask = 7
|
||||
self.block_delete = False
|
||||
self.call_level = 0
|
||||
self.command = ''
|
||||
self.current_line = 0
|
||||
self.current_vel = 0.0
|
||||
self.cycle_time = 0.0
|
||||
self.debug = 0
|
||||
self.delay_left = 0.0
|
||||
self.din = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.distance_to_go = 0.0
|
||||
self.dout = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.dtg = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.echo_serial_number = 0
|
||||
self.enabled = False
|
||||
self.estop = 0
|
||||
self.exec_state = 0
|
||||
self.feed_hold_enabled = False
|
||||
self.feed_override_enabled = False
|
||||
self.feedrate = 0.0
|
||||
self.file = ''
|
||||
self.flood = 0
|
||||
self.g5x_index = 1
|
||||
self.g5x_offset = (0.0, 0.0, -1.61875, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.g92_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.gcodes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.homed = (0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.motion_id = 0
|
||||
self.inpos = False
|
||||
self.input_timeout = False
|
||||
self.interp_state = 0
|
||||
self.interpreter_errcode = 0
|
||||
self.joint = ({'ferror_current': 0.0, 'max_position_limit': 40.0, 'max_ferror': 0.05, 'inpos': 1, 'ferror_highmark': 3.85175736e-09, 'jointType': 1, 'units': 0.03937007874015748, 'input': 3.85175736e-09, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 0.01, 'max_hard_limit': 0, 'output': 3.85175736e-09, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -40.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 40.0, 'max_ferror': 0.05, 'inpos': 1, 'ferror_highmark': 3.85175747e-09, 'jointType': 1, 'units': 0.03937007874015748, 'input': 3.85175747e-09, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 0.01, 'max_hard_limit': 0, 'output': 3.85175747e-09, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -40.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 0.12, 'max_ferror': 0.05, 'inpos': 1, 'ferror_highmark': 2.95467673e-09, 'jointType': 1, 'units': 0.03937007874015748, 'input': 2.95467673e-09, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 0.01, 'max_hard_limit': 0, 'output': 2.95467673e-09, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -8.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0}, {'ferror_current': 0.0, 'max_position_limit': 1.0, 'max_ferror': 1.0, 'inpos': 1, 'ferror_highmark': 0.0, 'jointType': 1, 'units': 1.0, 'input': 0.0, 'min_soft_limit': 0, 'min_hard_limit': 0, 'homing': 0, 'min_ferror': 1.0, 'max_hard_limit': 0, 'output': 0.0, 'backlash': 0.0, 'fault': 0, 'enabled': 0, 'max_soft_limit': 0, 'override_limits': 0, 'homed': 0, 'min_position_limit': -1.0, 'velocity': 0.0})
|
||||
self.joint_actual_position = (3.85175736e-09, 3.85175747e-09, 2.95467673e-09, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.joint_position = (3.85175736e-09, 3.85175747e-09, 2.95467673e-09, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.joints = 3
|
||||
self.kinematics_type = 1
|
||||
self.limit = (0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.linear_units = 0.0393700787402
|
||||
self.max_acceleration = 0.0
|
||||
self.max_velocity = 0.0
|
||||
self.mcodes = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.mist = 0
|
||||
self.motion_line = 0
|
||||
self.motion_mode = 0
|
||||
self.motion_type = 0
|
||||
self.optional_stop = False
|
||||
self.paused = False
|
||||
self.pocket_prepped = 0
|
||||
self.position = (3.85175736e-09, 3.85175747e-09, 2.95467673e-09, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.probe_tripped = False
|
||||
self.probe_val = 0
|
||||
self.probed_position = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.probing = False
|
||||
self.program_units = 0
|
||||
self.queue = 0
|
||||
self.queue_full = False
|
||||
self.queued_mdi_commands = 0
|
||||
self.rapidrate = 0.0
|
||||
self.read_line = 0
|
||||
self.rotation_xy = 0.0
|
||||
self.settings = (0.0, 0.0, 0.0)
|
||||
self.spindle = ({'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0}, {'direction': 0, 'orient_state': 0, 'enabled': 0, 'override_enabled': False, 'brake': 0, 'homed': False, 'override': 0.0, 'speed': 0.0, 'orient_fault': 0})
|
||||
self.spindles = 0
|
||||
self.state = 0
|
||||
self.task_mode = 0
|
||||
self.task_paused = 0
|
||||
self.task_state = 0
|
||||
self.tool_in_spindle = 0
|
||||
self.tool_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
self.tool_table = (
|
||||
(-1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(1, 0.125, 0.511, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(2, 0.0625, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(3, 0.201, 1.273, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(4, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(8, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(10, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(11, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(12, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(13, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(14, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(16, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(17, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(18, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(19, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(20, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(21, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(22, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(23, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(24, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(26, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(27, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(28, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(29, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(30, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(31, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(33, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(34, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(35, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(36, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(37, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(38, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(39, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(40, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(41, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(42, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(44, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(45, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(46, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(47, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(48, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(49, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(50, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(51, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(52, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(54, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
(55, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0),
|
||||
)
|
||||
self.velocity = 0.0
|
||||
|
||||
def poll(self):
|
||||
return True
|
||||
749
lib/python/qtvcp/widgets/file_manager.py
Normal file
@@ -0,0 +1,749 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
|
||||
from qtpy.QtCore import QCoreApplication
|
||||
from qtpy.QtWidgets import (QApplication, QFileSystemModel, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QListView, QComboBox, QPushButton, QToolButton, QSizePolicy,
|
||||
QMenu, QAction, QLineEdit, QCheckBox, QTableView, QHeaderView,
|
||||
QMessageBox)
|
||||
from qtpy.QtGui import QIcon, QDragEnterEvent, QDropEvent
|
||||
from qtpy.QtCore import (QModelIndex, QDir, Qt, Slot, Signal, Property, QFileInfo, QMimeData,
|
||||
QItemSelectionModel, QItemSelection, QSortFilterProxyModel)
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# ACTION gives commands to linuxcnc
|
||||
# INFO holds INI file details
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
_translate = QCoreApplication.translate
|
||||
|
||||
class SharedViewMixin:
|
||||
itemDropped = Signal(list)
|
||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dragMoveEvent(self, event: QDragEnterEvent):
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
for url in event.mimeData().urls():
|
||||
source_path = url.toLocalFile()
|
||||
proxy_index = self.indexAt(event.pos())
|
||||
if proxy_index.isValid():
|
||||
source_index = self.proxy_model.mapToSource(proxy_index)
|
||||
if source_index.isValid():
|
||||
destination_path = self.source_model.filePath(source_index)
|
||||
if os.path.isdir(destination_path):
|
||||
dest_file = os.path.join(destination_path, os.path.basename(source_path))
|
||||
else:
|
||||
dest_file = os.path.join(os.path.dirname(destination_path), os.path.basename(source_path))
|
||||
else:
|
||||
destination_path = self.source_model.rootPath()
|
||||
dest_file = os.path.join(destination_path, os.path.basename(source_path))
|
||||
# isdir = True if os.path.isdir(source_path) else False
|
||||
self.itemDropped.emit([source_path, dest_file])
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
|
||||
class CustomTableView(QTableView, SharedViewMixin):
|
||||
def __init__(self, proxy_model, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setModel(proxy_model)
|
||||
self.proxy_model = proxy_model
|
||||
self.source_model = proxy_model.sourceModel()
|
||||
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setDragDropMode(QTableView.DragDrop)
|
||||
|
||||
|
||||
class CustomListView(QListView, SharedViewMixin):
|
||||
def __init__(self, proxy_model, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setModel(proxy_model)
|
||||
self.proxy_model = proxy_model
|
||||
self.source_model = proxy_model.sourceModel()
|
||||
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setDragDropMode(QListView.DragDrop)
|
||||
|
||||
|
||||
class RestrictedProxyModel(QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.base_path = '/home'
|
||||
self.restricted = False
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
if not self.restricted:
|
||||
return True
|
||||
model = self.sourceModel()
|
||||
index = model.index(source_row, 0, source_parent)
|
||||
if not index.isValid():
|
||||
return False
|
||||
# Only allow paths inside the base path
|
||||
file_path = QDir(model.filePath(index)).canonicalPath()
|
||||
return file_path.startswith(self.base_path)
|
||||
|
||||
def set_rootpath(self, path):
|
||||
self.base_path = path
|
||||
|
||||
def get_rootpath(self):
|
||||
return self.base_path
|
||||
|
||||
def set_restricted(self, state: bool):
|
||||
self.restricted = state
|
||||
self.invalidateFilter()
|
||||
|
||||
def lessThan(self, left, right):
|
||||
# name
|
||||
if left.column() == 0:
|
||||
left_data = self.sourceModel().fileInfo(left)
|
||||
right_data = self.sourceModel().fileInfo(right)
|
||||
if left_data.isDir() and not right_data.isDir():
|
||||
return True
|
||||
if not left_data.isDir() and right_data.isDir():
|
||||
return False
|
||||
return left_data.fileName().lower() < right_data.fileName().lower()
|
||||
|
||||
# size
|
||||
if left.column() == 1:
|
||||
left_data = self.sourceModel().size(left)
|
||||
right_data = self.sourceModel().size(right)
|
||||
return left_data < right_data
|
||||
|
||||
# last modified
|
||||
if left.column() == 3:
|
||||
left_data = self.sourceModel().fileInfo(left)
|
||||
right_data = self.sourceModel().fileInfo(right)
|
||||
return left_data.lastModified() < right_data.lastModified()
|
||||
|
||||
# default
|
||||
return False
|
||||
|
||||
class FileManager(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(FileManager, self).__init__(parent)
|
||||
self.title = _translate('FileManager','Qtvcp File System View')
|
||||
self._last = 0
|
||||
self._doubleClick = False
|
||||
self._showListView = False
|
||||
|
||||
if INFO.PROGRAM_PREFIX is not None:
|
||||
self.user_path = os.path.expanduser(INFO.PROGRAM_PREFIX)
|
||||
else:
|
||||
self.user_path = (os.path.join(os.path.expanduser('~'), 'linuxcnc/nc_files'))
|
||||
user = os.path.split(os.path.expanduser('~') )[-1]
|
||||
|
||||
# check for Ubuntu/Mint path first
|
||||
media = os.path.join('/media', user)
|
||||
if os.path.exists(media):
|
||||
self.media_path = media
|
||||
else:
|
||||
self.media_path = '/media'
|
||||
temp = [('User', self.user_path), ('Media', self.media_path)]
|
||||
self._jumpList = OrderedDict(temp)
|
||||
self.currentPath = None
|
||||
self.currentFolder = None
|
||||
self.jump_delete = []
|
||||
self.PREFS_ = None
|
||||
self.initUI()
|
||||
|
||||
def initUI(self):
|
||||
self.setWindowTitle(self.title)
|
||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
|
||||
line_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
button_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
box_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
|
||||
self.textLine = QLineEdit()
|
||||
self.textLine.setToolTip('Current Directory/selected File')
|
||||
self.textLine.setSizePolicy(line_policy)
|
||||
self.textLine.setMinimumHeight(40)
|
||||
self.pasteButton = QPushButton()
|
||||
self.pasteButton.setSizePolicy(button_policy)
|
||||
self.pasteButton.setMinimumSize(80, 40)
|
||||
self.pasteButton.setEnabled(False)
|
||||
self.pasteButton.setText('Paste')
|
||||
self.pasteButton.setToolTip('Copy file from copy path to current directory/file')
|
||||
self.pasteButton.clicked.connect(self.paste)
|
||||
self.pasteButton.hide()
|
||||
pasteBox = QHBoxLayout()
|
||||
pasteBox.addWidget(self.textLine)
|
||||
pasteBox.addWidget(self.pasteButton)
|
||||
|
||||
self.copyLine = QLineEdit()
|
||||
self.copyLine.setSizePolicy(line_policy)
|
||||
self.copyLine.setToolTip('File path to copy from when pasting')
|
||||
self.copyLine.setMinimumHeight(40)
|
||||
self.copyLine.setReadOnly(True)
|
||||
self.copyButton = QPushButton()
|
||||
self.copyButton.setSizePolicy(button_policy)
|
||||
self.copyButton.setMinimumSize(80, 40)
|
||||
self.copyButton.setText('Copy')
|
||||
self.copyButton.setToolTip('Record current file as copy path')
|
||||
self.copyButton.clicked.connect(self.recordCopyPath)
|
||||
self.copyBox = QHBoxLayout()
|
||||
self.copyBox.addWidget(self.copyButton)
|
||||
self.copyBox.addWidget(self.copyLine)
|
||||
self.copyLine.hide()
|
||||
self.copyButton.hide()
|
||||
|
||||
self.model = QFileSystemModel()
|
||||
self.model.setRootPath(QDir.currentPath())
|
||||
self.model.setFilter(QDir.AllDirs | QDir.NoDot | QDir.Files)
|
||||
self.model.setNameFilterDisables(False)
|
||||
self.model.rootPathChanged.connect(self.folderChanged)
|
||||
|
||||
self.proxy_model = RestrictedProxyModel()
|
||||
self.proxy_model.setSourceModel(self.model)
|
||||
|
||||
self.list = CustomListView(self.proxy_model)
|
||||
self.list.setModel(self.proxy_model)
|
||||
self.list.resize(640, 480)
|
||||
self.list.setAlternatingRowColors(True)
|
||||
self.list.hide()
|
||||
|
||||
self.table = CustomTableView(self.proxy_model)
|
||||
self.table.setModel(self.proxy_model)
|
||||
self.table.resize(640, 480)
|
||||
self.table.setAlternatingRowColors(True)
|
||||
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
header.swapSections(1,3)
|
||||
header.setSortIndicator(1,Qt.AscendingOrder)
|
||||
|
||||
self.table.setSortingEnabled(True)
|
||||
self.table.setColumnHidden(2, True) # type
|
||||
self.table.verticalHeader().setVisible(False) # row count header
|
||||
|
||||
self.cb = QComboBox()
|
||||
self.cb.currentIndexChanged.connect(self.filterChanged)
|
||||
self.fillCombobox(INFO.PROGRAM_FILTERS_EXTENSIONS)
|
||||
self.cb.setMinimumSize(200,40)
|
||||
self.cb.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
|
||||
self.jumpButton = QToolButton()
|
||||
self.jumpButton.setText(_translate('FileManager','User'))
|
||||
self.jumpButton.setSizePolicy(line_policy)
|
||||
self.jumpButton.setMinimumSize(40, 40)
|
||||
self.jumpButton.setMaximumSize(80, 40)
|
||||
self.jumpButton.setToolTip('Jump to User directory.\nLong press for Options.')
|
||||
self.jumpButton.clicked.connect(self.onJumpClicked)
|
||||
|
||||
self.addButton = QPushButton()
|
||||
self.addButton.setText(_translate('FileManager','Add\nJump'))
|
||||
self.addButton.setSizePolicy(line_policy)
|
||||
self.addButton.setMinimumSize(40, 40)
|
||||
self.addButton.setMaximumSize(80, 40)
|
||||
self.addButton.setToolTip('Add current directory to jump button list')
|
||||
self.addButton.clicked.connect(self.onActionClicked)
|
||||
|
||||
self.delButton = QPushButton()
|
||||
self.delButton.setText(_translate('FileManager','Del\nJump'))
|
||||
self.delButton.setSizePolicy(line_policy)
|
||||
self.delButton.setMinimumSize(40, 40)
|
||||
self.delButton.setMaximumSize(80, 40)
|
||||
self.delButton.setToolTip('Delete current directory from jump button list')
|
||||
self.delButton.clicked.connect(self.onActionClicked)
|
||||
|
||||
self.loadButton = QPushButton()
|
||||
self.loadButton.setText(_translate('FileManager','Load'))
|
||||
self.loadButton.setSizePolicy(line_policy)
|
||||
self.loadButton.setMinimumSize(40, 40)
|
||||
self.loadButton.setMaximumSize(80, 40)
|
||||
self.loadButton.setToolTip('Load selected file')
|
||||
self.loadButton.clicked.connect(self._getPathActivated)
|
||||
|
||||
self.chk_restricted = QCheckBox(_translate('FileManager','Restricted'))
|
||||
self.chk_restricted.setSizePolicy(box_policy)
|
||||
self.chk_restricted.setToolTip('Restrict navigation past root path')
|
||||
self.chk_restricted.stateChanged.connect(lambda state: self.setRestricted(state))
|
||||
|
||||
self.copy_control = QCheckBox(_translate('FileManager','Copy Controls'))
|
||||
self.copy_control.setSizePolicy(box_policy)
|
||||
self.copy_control.stateChanged.connect(lambda state: self.showCopyControls(state))
|
||||
|
||||
self.checkbox = QHBoxLayout()
|
||||
self.checkbox.addWidget(self.chk_restricted)
|
||||
self.checkbox.addWidget(self.copy_control)
|
||||
self.checkbox.insertStretch(2, stretch=0)
|
||||
|
||||
self.settingMenu = QMenu(self)
|
||||
self.jumpButton.setMenu(self.settingMenu)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(self.jumpButton)
|
||||
hbox.addWidget(self.addButton)
|
||||
hbox.addWidget(self.delButton)
|
||||
hbox.addWidget(self.loadButton)
|
||||
hbox.addWidget(self.cb)
|
||||
|
||||
self.windowLayout = QVBoxLayout()
|
||||
self.windowLayout.addLayout(pasteBox)
|
||||
self.windowLayout.addLayout(self.copyBox)
|
||||
self.windowLayout.addWidget(self.list)
|
||||
self.windowLayout.addWidget(self.table)
|
||||
self.windowLayout.addLayout(hbox)
|
||||
self.windowLayout.addLayout(self.checkbox)
|
||||
self.setLayout(self.windowLayout)
|
||||
|
||||
def _hal_init(self):
|
||||
if self.PREFS_:
|
||||
last_path = self.PREFS_.getpref('last_loaded_directory', self.user_path, str, 'BOOK_KEEPING')
|
||||
LOG.debug(f"LAST FILE PATH: {last_path}")
|
||||
if not last_path == '' and os.path.exists(last_path):
|
||||
self.updateDirectoryView(last_path)
|
||||
else:
|
||||
self.updateDirectoryView(self.user_path)
|
||||
|
||||
# get all the saved jumplist paths
|
||||
temp = self.PREFS_.getall('FILEMANAGER_JUMPLIST')
|
||||
self._jumpList.update(temp)
|
||||
|
||||
else:
|
||||
LOG.debug(f"LAST FILE PATH: {self.user_path}")
|
||||
self.updateDirectoryView(self.user_path)
|
||||
|
||||
# install jump paths into toolbutton menu
|
||||
for i in self._jumpList:
|
||||
self.addAction(i)
|
||||
|
||||
# set recorded columns sort settings
|
||||
self.SETTINGS_.beginGroup(f"FileManager-{self.objectName()}")
|
||||
sect = self.SETTINGS_.value('sortIndicatorSection', type = int)
|
||||
order = self.SETTINGS_.value('sortIndicatorOrder', type = int)
|
||||
self.SETTINGS_.endGroup()
|
||||
if not None in(sect,order):
|
||||
self.table.horizontalHeader().setSortIndicator(sect, Qt.SortOrder(order))
|
||||
|
||||
self.connectSelection()
|
||||
|
||||
# when qtvcp closes this gets called
|
||||
# record jump list paths
|
||||
def _hal_cleanup(self):
|
||||
if self.PREFS_:
|
||||
for opt in self.jump_delete:
|
||||
self.PREFS_.removepref(opt, 'FILEMANAGER_JUMPLIST')
|
||||
for i, key in enumerate(self._jumpList):
|
||||
if i in(0,1):
|
||||
continue
|
||||
self.PREFS_.putpref(key, self._jumpList.get(key), str, 'FILEMANAGER_JUMPLIST')
|
||||
|
||||
# record sorted columns
|
||||
h = self.table.horizontalHeader()
|
||||
self.SETTINGS_.beginGroup(f"FileManager-{self.objectName()}")
|
||||
self.SETTINGS_.setValue('sortIndicatorSection', h.sortIndicatorSection())
|
||||
self.SETTINGS_.setValue('sortIndicatorOrder', h.sortIndicatorOrder())
|
||||
self.SETTINGS_.endGroup()
|
||||
|
||||
#########################
|
||||
# callbacks
|
||||
#########################
|
||||
|
||||
# add shown text and hidden filter data from the INI
|
||||
def fillCombobox(self, data):
|
||||
for i in data:
|
||||
self.cb.addItem(i[0],i[1])
|
||||
|
||||
def folderChanged(self, data):
|
||||
data = os.path.normpath(data)
|
||||
self.currentFolder = data
|
||||
self.textLine.setText(data)
|
||||
|
||||
def updateDirectoryView(self, path, quiet = False):
|
||||
if os.path.exists(path):
|
||||
self.list.selectionModel().clearSelection()
|
||||
self.list.setRootIndex(QModelIndex())
|
||||
self.table.selectionModel().clearSelection()
|
||||
self.table.setRootIndex(QModelIndex())
|
||||
index = self.model.setRootPath(path)
|
||||
if index.isValid():
|
||||
proxy_index = self.proxy_model.mapFromSource(index)
|
||||
self.list.setRootIndex(proxy_index)
|
||||
self.table.setRootIndex(proxy_index)
|
||||
self.proxy_model.invalidate()
|
||||
else:
|
||||
LOG.debug(f"Set directory view error - no such path {path}")
|
||||
if not quiet:
|
||||
STATUS.emit('error', STATUS.TEMPORARY_MESSAGE, f"File Manager error - No such path: {path}")
|
||||
|
||||
# retrieve selected filter (it's held as QT.userData)
|
||||
def filterChanged(self, index):
|
||||
userdata = self.cb.itemData(index)
|
||||
self.model.setNameFilters(userdata)
|
||||
|
||||
def listClicked(self, index):
|
||||
# the signal passes the index of the clicked item
|
||||
source_index = self.proxy_model.mapToSource(index)
|
||||
dir_path = os.path.normpath(self.model.filePath(source_index))
|
||||
if self.model.fileInfo(source_index).isFile():
|
||||
self.currentPath = dir_path
|
||||
self.textLine.setText(self.currentPath)
|
||||
return
|
||||
else:
|
||||
self.currentPath = None
|
||||
self.textLine.setText(os.path.abspath(dir_path))
|
||||
self.updateDirectoryView(dir_path)
|
||||
|
||||
def onUserClicked(self):
|
||||
self.showUserDir()
|
||||
|
||||
def onMediaClicked(self):
|
||||
self.showMediaDir()
|
||||
|
||||
# jump directly to a saved path shown on the button
|
||||
def onJumpClicked(self):
|
||||
data = self.jumpButton.text()
|
||||
if data.upper() == 'MEDIA':
|
||||
self.showMediaDir()
|
||||
elif data.upper() == 'USER':
|
||||
self.showUserDir()
|
||||
elif data in self._jumpList:
|
||||
temp = self._jumpList[data]
|
||||
if temp is not None:
|
||||
self.updateDirectoryView(temp)
|
||||
else:
|
||||
STATUS.emit('error',STATUS.OPERATOR_ERROR, f'file jumppath: {data} not valid')
|
||||
LOG.debug(f'file jumppath: {data} not valid')
|
||||
else:
|
||||
self.jumpButton.setText('User')
|
||||
|
||||
# jump directly to a saved path from the menu
|
||||
def jumpTriggered(self, data):
|
||||
name = data
|
||||
self.jumpButton.setText(name)
|
||||
if data.upper() == 'MEDIA':
|
||||
self.jumpButton.setToolTip('Jump to Media directory.\nLong press for Options.')
|
||||
self.showMediaDir()
|
||||
elif data.upper() == 'USER':
|
||||
self.jumpButton.setToolTip('Jump to User directory.\nLong press for Options.')
|
||||
self.showUserDir()
|
||||
else:
|
||||
self.jumpButton.setToolTip(f'Jump to directory:\n{self._jumpList.get(name)}')
|
||||
self.updateDirectoryView(self._jumpList.get(name))
|
||||
|
||||
# add or remove a jump list path
|
||||
def onActionClicked(self):
|
||||
name = os.path.basename(self.currentFolder)
|
||||
btn = self.sender()
|
||||
if btn == self.addButton:
|
||||
try:
|
||||
self._jumpList[name] = self.currentFolder
|
||||
self.addAction(name)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
elif btn == self.delButton:
|
||||
try:
|
||||
x = self.jumpButton.text()
|
||||
if name in self._jumpList:
|
||||
self.jump_delete.append(name)
|
||||
self._jumpList.pop(name)
|
||||
elif x in self._jumpList:
|
||||
self.jump_delete.append(x)
|
||||
self._jumpList.pop(x)
|
||||
else: return # don't know what to do otherwise
|
||||
self.settingMenu.clear()
|
||||
for key in self._jumpList:
|
||||
self.addAction(key)
|
||||
self.jumpButton.setText('User')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# get current selection and update the path
|
||||
# then if the path is good load it into linuxcnc
|
||||
# record it in the preference file if available
|
||||
def _getPathActivated(self):
|
||||
if self.list.isVisible():
|
||||
row = self.list.selectionModel().currentIndex()
|
||||
else:
|
||||
row = self.table.selectionModel().currentIndex()
|
||||
self.listClicked(row)
|
||||
|
||||
fname = self.currentPath
|
||||
if fname is None:
|
||||
return
|
||||
if fname:
|
||||
self.load(fname)
|
||||
|
||||
def recordCopyPath(self):
|
||||
data, isFile = self.getCurrentSelected()
|
||||
if isFile:
|
||||
self.copyLine.setText(os.path.normpath(data))
|
||||
self.pasteButton.setEnabled(True)
|
||||
else:
|
||||
self.copyLine.setText('')
|
||||
self.pasteButton.setEnabled(False)
|
||||
STATUS.emit('error', STATUS.OPERATOR_ERROR, 'Can only copy a file, not a folder')
|
||||
|
||||
def paste(self):
|
||||
res = self.copyFile(self.copyLine.text(), self.textLine.text())
|
||||
if res:
|
||||
self.copyLine.setText('')
|
||||
self.pasteButton.setEnabled(False)
|
||||
|
||||
########################
|
||||
# helper functions
|
||||
########################
|
||||
|
||||
def setRestricted(self, state):
|
||||
self.proxy_model.set_restricted(state)
|
||||
self.updateDirectoryView(self.proxy_model.get_rootpath())
|
||||
|
||||
def addAction(self, i):
|
||||
action = QAction(QIcon.fromTheme('user-home'), i, self)
|
||||
# weird lambda i=i to work around 'function closure'
|
||||
action.triggered.connect(lambda state, i=i: self.jumpTriggered(i))
|
||||
self.settingMenu.addAction(action)
|
||||
|
||||
def showList(self, state=True):
|
||||
if state:
|
||||
self.table.hide()
|
||||
self.list.show()
|
||||
else:
|
||||
self.table.show()
|
||||
self.list.hide()
|
||||
|
||||
def showTable(self, state=True):
|
||||
self.showList(not state)
|
||||
|
||||
def showCopyControls(self, state):
|
||||
if state:
|
||||
self.copyLine.show()
|
||||
self.copyButton.show()
|
||||
self.pasteButton.show()
|
||||
else:
|
||||
self.copyLine.hide()
|
||||
self.copyButton.hide()
|
||||
self.pasteButton.hide()
|
||||
|
||||
def showMediaDir(self, quiet = False):
|
||||
self.proxy_model.set_rootpath(self.media_path)
|
||||
self.jumpButton.setText(_translate('FileManager','Media'))
|
||||
self.updateDirectoryView(self.media_path, quiet)
|
||||
|
||||
def showUserDir(self, quiet = False):
|
||||
self.proxy_model.set_rootpath(self.user_path)
|
||||
self.jumpButton.setText(_translate('FileManager','User'))
|
||||
self.updateDirectoryView(self.user_path, quiet)
|
||||
|
||||
def dropCopy(self, data):
|
||||
source, dest = data
|
||||
self.copyChecks(source, dest)
|
||||
|
||||
def copyFile(self, source, dest):
|
||||
self.copyChecks(source, dest)
|
||||
|
||||
def overwriteMessage(self, d):
|
||||
title = "File Already Exists"
|
||||
info = f"Overwrite {d}?"
|
||||
buttons = QMessageBox.No | QMessageBox.Yes
|
||||
retval = QMessageBox.warning(
|
||||
self, title, info, buttons)
|
||||
if retval == QMessageBox.No:
|
||||
return False
|
||||
return True
|
||||
|
||||
# likely class patch candidate
|
||||
def copyChecks(self, s, d):
|
||||
if os.path.isfile(d) or os.path.isdir(d):
|
||||
retval = self.overwriteMessage(d)
|
||||
if retval == False:
|
||||
txt = f'File {d} not copied'
|
||||
mess = {'SHORTTEXT':txt,'TITLE':'FileManager',
|
||||
'DETAILS':txt}
|
||||
opt = {'LOG':True,'LEVEL':STATUS.WARNING}
|
||||
STATUS.emit('status-message',mess,opt)
|
||||
return False
|
||||
self.copy(s, d)
|
||||
|
||||
def copy(self, s, d):
|
||||
try:
|
||||
if os.path.isdir(s):
|
||||
shutil.copytree(s, d)
|
||||
else:
|
||||
shutil.copy2(s, d)
|
||||
txt = f' Copied {s} to {d}'
|
||||
mess = {'SHORTTEXT':txt,'TITLE':'FileManager',
|
||||
'DETAILS':txt}
|
||||
opt = {'LOG':True,'LEVEL':STATUS.DEFAULT}
|
||||
STATUS.emit('status-message',mess,opt)
|
||||
return True
|
||||
except Exception as e:
|
||||
STATUS.emit('error', STATUS.OPERATOR_ERROR, f"Copy file error: {e}")
|
||||
return False
|
||||
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def scroll(self, data):
|
||||
if data > self._last:
|
||||
self.up()
|
||||
elif data < self._last:
|
||||
self.down()
|
||||
self._last = data
|
||||
|
||||
# moves the selection up
|
||||
# used with MPG scrolling
|
||||
def up(self):
|
||||
self.select_row('up')
|
||||
|
||||
# moves the selection down
|
||||
# used with MPG scrolling
|
||||
def down(self):
|
||||
self.select_row('down')
|
||||
|
||||
def select_row(self, style='down'):
|
||||
style = style.lower()
|
||||
if self.list.isVisible():
|
||||
i = self.list.rootIndex()
|
||||
selectionModel = self.list.selectionModel()
|
||||
else:
|
||||
i = self.table.rootIndex()
|
||||
selectionModel = self.table.selectionModel()
|
||||
|
||||
row = selectionModel.currentIndex().row()
|
||||
self.rows = self.model.rowCount(i)
|
||||
|
||||
if style == 'last':
|
||||
row = self.rows
|
||||
elif style == 'up':
|
||||
if row > 0:
|
||||
row -= 1
|
||||
else:
|
||||
row = 0
|
||||
elif style == 'down':
|
||||
if row < self.rows-1:
|
||||
row += 1
|
||||
else:
|
||||
row = self.rows-1
|
||||
else:
|
||||
return
|
||||
top = self.model.index(row, 0, i)
|
||||
selectionModel.setCurrentIndex(top, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
||||
selection = QItemSelection(top, top)
|
||||
selectionModel.clearSelection()
|
||||
selectionModel.select(selection, QItemSelectionModel.Select)
|
||||
|
||||
# returns the current highlighted (selected) path as well as
|
||||
# whether it's a file or not.
|
||||
def getCurrentSelected(self):
|
||||
if self.list.isVisible():
|
||||
selectionModel = self.list.selectionModel()
|
||||
else:
|
||||
selectionModel = self.table.selectionModel()
|
||||
index = self.proxy_model.mapToSource(selectionModel.currentIndex())
|
||||
dir_path = os.path.normpath(self.model.filePath(index))
|
||||
if self.model.fileInfo(index).isFile():
|
||||
return (dir_path, True)
|
||||
elif dir_path == '.':
|
||||
return (self.proxy_model.get_rootpath(), False)
|
||||
else:
|
||||
return (dir_path, False)
|
||||
|
||||
# This can be class patched to do something else
|
||||
def load(self, fname=None):
|
||||
return
|
||||
# try:
|
||||
# if fname is None:
|
||||
# self._getPathActivated()
|
||||
# return
|
||||
# self.recordBookKeeping()
|
||||
# ACTION.OPEN_PROGRAM(fname)
|
||||
# STATUS.emit('update-machine-log', f'Loaded: {fname}', 'TIME,SUCCESS')
|
||||
# except Exception as e:
|
||||
# LOG.error(f"Load file error: {e}")
|
||||
# STATUS.emit('error', STATUS.NML_ERROR, f"Load file error: {e}")
|
||||
|
||||
# This can be class patched to do something else
|
||||
def recordBookKeeping(self):
|
||||
fname = self.currentPath
|
||||
if fname is None:
|
||||
return
|
||||
if self.PREFS_:
|
||||
self.PREFS_.putpref('last_loaded_directory', self.model.rootPath(), str, 'BOOK_KEEPING')
|
||||
self.PREFS_.putpref('RecentPath_0', fname, str, 'BOOK_KEEPING')
|
||||
|
||||
def connectSelection(self):
|
||||
try:
|
||||
self.list.disconnect()
|
||||
self.table.disconnect()
|
||||
self.list.activated.connect(self._getPathActivated)
|
||||
self.table.activated.connect(self._getPathActivated)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.list.itemDropped.connect(self.dropCopy)
|
||||
self.table.itemDropped.connect(self.dropCopy)
|
||||
|
||||
# choose double click or single click for folder selection
|
||||
if self._doubleClick:
|
||||
self.list.doubleClicked[QModelIndex].connect(self.listClicked)
|
||||
self.table.doubleClicked[QModelIndex].connect(self.listClicked)
|
||||
else:
|
||||
self.list.clicked[QModelIndex].connect(self.listClicked)
|
||||
self.table.clicked[QModelIndex].connect(self.listClicked)
|
||||
|
||||
######################
|
||||
# Properties
|
||||
######################
|
||||
|
||||
# Double Click folder selection
|
||||
def setDoubleClickSelection(self, state):
|
||||
self._doubleClick = state
|
||||
self.connectSelection()
|
||||
def getDoubleClickSelection(self):
|
||||
return self._doubleClick
|
||||
def resetDoubleClickSelection(self, state):
|
||||
self._doubleClick = False
|
||||
self.connectSelection()
|
||||
doubleClickSelection = Property(bool, getDoubleClickSelection, setDoubleClickSelection, resetDoubleClickSelection)
|
||||
|
||||
# list/table view selection
|
||||
def setShowListView(self, state):
|
||||
self._showListView = state
|
||||
self.showList(state)
|
||||
def getShowListView(self):
|
||||
return self._showListView
|
||||
def resetShowListView(self, state):
|
||||
self._showListView = False
|
||||
self.showList(False)
|
||||
showListView = Property(bool, getShowListView, setShowListView, resetShowListView)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QApplication(sys.argv)
|
||||
gui = FileManager()
|
||||
gui.show()
|
||||
gui.PREFS_ = True
|
||||
gui.connectSelection()
|
||||
gui.onUserClicked()
|
||||
# gui.onMediaClicked()
|
||||
gui.setRestricted(True)
|
||||
sys.exit(app.exec())
|
||||
|
||||
1230
lib/python/qtvcp/widgets/gcode_editor.py
Normal file
595
lib/python/qtvcp/widgets/gcode_graphics.py
Normal file
@@ -0,0 +1,595 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
"""
|
||||
PyQt5 widget for plotting gcode.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import gcode
|
||||
import linuxcnc
|
||||
from qtpy.QtCore import Property, QTimer, Qt
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
from qt5_graphics import Lcnc_3dGraphics
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INFO is INI file details
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
##############################################
|
||||
# Container class
|
||||
##############################################
|
||||
class GCodeGraphics(Lcnc_3dGraphics, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super( GCodeGraphics, self).__init__(parent)
|
||||
|
||||
self._overlayColor = QColor(0, 0, 0, 0)
|
||||
self.colors['overlay_alpha'] = 0.75
|
||||
self._overlayAlpha = 0.75
|
||||
self._gridColor = QColor(0, 138, 138)
|
||||
self._backgroundColor = QColor(0, 0, 191, 150)
|
||||
self._jogColor = QColor(0, 0, 0, 0)
|
||||
self._feedColor = QColor(0, 0, 0, 0)
|
||||
self._rapidColor = QColor(0, 0, 0, 0)
|
||||
self.use_gradient_background = False
|
||||
# color1 is the bottom color that blends up to color2
|
||||
self.gradient_color1 = (0.,0,.5)
|
||||
self.gradient_color2 = (0,.0, 0)
|
||||
|
||||
self.show_overlay = False # no DRO or DRO overlay
|
||||
self._reload_filename = None
|
||||
self.show_small_origin = True
|
||||
|
||||
self._view_incr = 20
|
||||
self.inhibit_selection = False
|
||||
self._block_line_selected = False
|
||||
|
||||
# stop response to external STATUS signals
|
||||
self._disable_STATUS_signals = False
|
||||
self._block_autoLoad = None
|
||||
self._block_reLoad = None
|
||||
self._block_viewChanged = None
|
||||
self._block_lineSelect = None
|
||||
|
||||
self._mouseMode = 0
|
||||
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
self.mach_units = 'Metric'
|
||||
else:
|
||||
self.mach_units = 'Imperial'
|
||||
|
||||
def addTimer(self):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.poll)
|
||||
self.timer.start(INFO.GRAPHICS_CYCLE_TIME)
|
||||
|
||||
def _hal_init(self):
|
||||
self._block_autoLoad = STATUS.connect('file-loaded', self.load_program)
|
||||
self._block_reLoad = STATUS.connect('reload-display', self.reloadfile)
|
||||
STATUS.connect('actual-spindle-speed-changed', self.set_spindle_speed)
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data))
|
||||
STATUS.connect('metric-mode-changed', lambda w, f: self.set_metric_units(w, f))
|
||||
self._block_viewChanged = STATUS.connect('graphics-view-changed', lambda w, v, a: self.set_view_signal(v, a))
|
||||
self._block_lineSelect = STATUS.connect('gcode-line-selected', lambda w, l: self.highlight_graphics(l))
|
||||
# we do this in this function because the property InhibitControls
|
||||
# is set before the STATUS (GObject) signal ids can be recorded
|
||||
if self._disable_STATUS_signals:
|
||||
self.updateSignals(True)
|
||||
|
||||
# If there is a preference file object use it to load the user view position data
|
||||
if self.PREFS_:
|
||||
v,z,x,y,lat,lon = self.getCurrentViewSettings()
|
||||
v = self.PREFS_.getpref(self.HAL_NAME_+'-user-view', v, str, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
z = self.PREFS_.getpref(self.HAL_NAME_+'-user-zoom', z, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
x = self.PREFS_.getpref(self.HAL_NAME_+'-user-panx', x, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
y = self.PREFS_.getpref(self.HAL_NAME_+'-user-pany', y, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
lat = self.PREFS_.getpref(self.HAL_NAME_+'-user-lat', lat, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
lon = self.PREFS_.getpref(self.HAL_NAME_+'-user-lon', lon, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.presetViewSettings(v,z,x,y,lat,lon)
|
||||
|
||||
self.set_preferred_view()
|
||||
|
||||
# when qtvcp closes, this gets called
|
||||
def _hal_cleanup(self):
|
||||
self.timer.stop()
|
||||
self.logger.stop()
|
||||
if self.PREFS_:
|
||||
v,z,x,y,lat,lon = self.getRecordedViewSettings()
|
||||
LOG.debug('Saving {} data to file.'.format(self.HAL_NAME_))
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-view', v, str, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-zoom', z, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-panx', x, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-pany', y, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-lat', lat, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-user-lon', lon, float, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-current-view', self.get_current_view(), str, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
|
||||
def set_preferred_view(self):
|
||||
if self.PREFS_:
|
||||
view = self.PREFS_.getpref(self.HAL_NAME_+'-current-view', 'p', str, 'SCREEN_CONTROL_LAST_SETTING')
|
||||
# set graphics view variable
|
||||
self.current_view = view
|
||||
# tell any other interested widgets that the view changed
|
||||
STATUS.emit('graphics-view-changed', self.current_view, None)
|
||||
|
||||
# external source asked for highlight,
|
||||
# make sure we block the propagation
|
||||
def highlight_graphics(self, line):
|
||||
if self._current_file is None: return
|
||||
self._block_line_selected = True
|
||||
self.set_highlight_line(line)
|
||||
|
||||
# used to clear highlighting externally
|
||||
def clear_highlight(self):
|
||||
self.set_highlight_line(None)
|
||||
|
||||
def set_view_signal(self, view, args):
|
||||
#print(view,args)
|
||||
v = view.lower()
|
||||
if v == 'clear':
|
||||
self.clear_live_plotter()
|
||||
elif v == 'zoom-in':
|
||||
self.zoomin()
|
||||
elif v == 'zoom-out':
|
||||
self.zoomout()
|
||||
elif v == 'pan-down':
|
||||
self.recordMouse(0,0)
|
||||
self.translateOrRotate(0,self._view_incr)
|
||||
elif v == 'pan-up':
|
||||
self.recordMouse(0,0)
|
||||
self.translateOrRotate(0,-self._view_incr)
|
||||
elif v == 'pan-right':
|
||||
self.recordMouse(0,0)
|
||||
self.translateOrRotate(self._view_incr,0)
|
||||
elif v == 'pan-left':
|
||||
self.recordMouse(0,0)
|
||||
self.translateOrRotate(-self._view_incr,0)
|
||||
elif v == 'rotate-ccw':
|
||||
self.recordMouse(0,0)
|
||||
self.rotateOrTranslate(self._view_incr,0)
|
||||
elif v == 'rotate-cw':
|
||||
self.recordMouse(0,0)
|
||||
self.rotateOrTranslate(-self._view_incr,0)
|
||||
elif v == 'rotate-up':
|
||||
self.recordMouse(0,0)
|
||||
self.rotateOrTranslate(0,self._view_incr)
|
||||
elif v == 'rotate-down':
|
||||
self.recordMouse(0,0)
|
||||
self.rotateOrTranslate(0,-self._view_incr)
|
||||
elif v == 'overlay-offsets-on':
|
||||
self.setShowOffsets(True)
|
||||
elif v == 'overlay-offsets-off':
|
||||
self.setShowOffsets(False)
|
||||
elif v == 'overlay-dro-on':
|
||||
self.setProperty('_dro',True)
|
||||
elif v == 'overlay-dro-off':
|
||||
self.setProperty('_dro',False)
|
||||
elif v == 'dtg-on':
|
||||
self.setProperty('_dtg',True)
|
||||
elif v == 'dtg-off':
|
||||
self.setProperty('_dtg',False)
|
||||
elif v == 'pan-view':
|
||||
self.panView(args.get('X'),args.get('Y'))
|
||||
elif v == 'rotate-view':
|
||||
self.rotateView(args.get('X'),args.get('Y'))
|
||||
elif v == 'grid-color':
|
||||
self.setProperty('grid_color',args.get('COLOR'))
|
||||
elif v == 'grid-off':
|
||||
self.grid_size = 0.0
|
||||
self.update()
|
||||
elif v == 'grid-size':
|
||||
self.grid_size = args.get('SIZE')
|
||||
self.update()
|
||||
elif v == 'alpha-mode-on':
|
||||
self.set_alpha_mode(True)
|
||||
elif v == 'alpha-mode-off':
|
||||
self.set_alpha_mode(False)
|
||||
elif v == 'inhibit-selection-on':
|
||||
self.inhibit_selection = True
|
||||
elif v == 'inhibit-selection-off':
|
||||
self.inhibit_selection = False
|
||||
elif v == 'dimensions-on':
|
||||
self.show_extents_option = True
|
||||
self.update()
|
||||
elif v == 'dimensions-off':
|
||||
self.show_extents_option = False
|
||||
self.update()
|
||||
elif v == 'record-view':
|
||||
self.recordCurrentViewSettings()
|
||||
elif v == 'set-recorded-view':
|
||||
self.setRecordedView()
|
||||
elif v == 'set-large-dro':
|
||||
self.set_font(True)
|
||||
elif v == 'set-small-dro':
|
||||
self.set_font(False)
|
||||
elif v == 'scroll-mode':
|
||||
m = args.get('MODE')
|
||||
self.setScrollMode(int(m))
|
||||
elif v in('x', 'y', 'y2', 'z', 'z2', 'p'):
|
||||
self.set_view(v)
|
||||
else:
|
||||
LOG.error('view signal not recognized: {}'.format(v))
|
||||
|
||||
def load_program(self, g, fname):
|
||||
LOG.debug('load the display: {}'.format(fname))
|
||||
self._reload_filename = fname
|
||||
result = self.load(fname)
|
||||
STATUS.emit('graphics-gcode-properties',self.gcode_properties)
|
||||
# reset the current view to standard calculated zoom and position
|
||||
self.set_current_view()
|
||||
return result
|
||||
|
||||
def set_metric_units(self, w, state):
|
||||
self.metric_units = state
|
||||
self.update()
|
||||
|
||||
def set_spindle_speed(self, w, rate):
|
||||
self.spindle_speed = int(rate)
|
||||
self.update()
|
||||
|
||||
def _tool_info(self, data):
|
||||
if data.id != -1:
|
||||
self._tool_dia = self.conversion(data.diameter)
|
||||
else:
|
||||
self._tool_dia = 0
|
||||
|
||||
# This does the conversion units
|
||||
# data must always be in machine units
|
||||
def conversion(self, data):
|
||||
if self.metric_units :
|
||||
return INFO.convert_machine_to_metric(data)
|
||||
else:
|
||||
return INFO.convert_machine_to_imperial(data)
|
||||
|
||||
def set_view(self, value):
|
||||
view = str(value).lower()
|
||||
if self.lathe_option:
|
||||
if view not in ['p', 'y', 'y2']:
|
||||
return False
|
||||
elif view not in ['p', 'x', 'y', 'z', 'z2']:
|
||||
return False
|
||||
self.current_view = view
|
||||
if self.initialised:
|
||||
self.set_current_view()
|
||||
|
||||
def reloadfile(self, w):
|
||||
LOG.debug('reload the display: {}'.format(self._reload_filename))
|
||||
try:
|
||||
self.load(self._reload_filename)
|
||||
self.clear_live_plotter()
|
||||
STATUS.emit('graphics-gcode-properties',self.gcode_properties)
|
||||
except:
|
||||
print('error', self._reload_filename)
|
||||
pass
|
||||
|
||||
def updateSignals(self, state):
|
||||
if self._block_autoLoad == None:
|
||||
return
|
||||
if state:
|
||||
STATUS.handler_block(self._block_autoLoad)
|
||||
STATUS.handler_block(self._block_reLoad)
|
||||
STATUS.handler_block(self._block_viewChanged)
|
||||
STATUS.handler_block(self._block_lineSelect)
|
||||
else:
|
||||
STATUS.handler_unblock(self._block_autoLoad)
|
||||
STATUS.handler_unblock(self._block_reLoad)
|
||||
STATUS.handler_unblock(self._block_viewChanged)
|
||||
STATUS.handler_unblock(self._block_lineSelect)
|
||||
|
||||
def updateMouseMode(self, value):
|
||||
if value == 0:
|
||||
m = Qt.LeftButton; z = Qt.MiddleButton; r = Qt.RightButton
|
||||
elif value == 1:
|
||||
m = Qt.MiddleButton; z = Qt.RightButton; r = Qt.LeftButton
|
||||
elif value == 2:
|
||||
m = Qt.MiddleButton; z = Qt.LeftButton; r = Qt.RightButton
|
||||
elif value == 3:
|
||||
m = Qt.LeftButton; z = Qt.RightButton; r = Qt.MiddleButton
|
||||
elif value == 4:
|
||||
m = Qt.RightButton; z = Qt.LeftButton; r = Qt.MiddleButton
|
||||
elif value == 5:
|
||||
m = Qt.RightButton; z = Qt.MiddleButton; r = Qt.LeftButton
|
||||
elif value == 6:
|
||||
m = Qt.LeftButton; z = Qt.MiddleButton; r = False
|
||||
elif value == 7:
|
||||
m = Qt.MiddleButton; z = Qt.LeftButton; r = False
|
||||
elif value == 8:
|
||||
m = Qt.RightButton; z = Qt.LeftButton; r = False
|
||||
elif value == 9:
|
||||
m = Qt.LeftButton; z = Qt.RightButton; r = False
|
||||
elif value == 10:
|
||||
m = Qt.MiddleButton; z = Qt.RightButton; r = False
|
||||
elif value == 11:
|
||||
m = Qt.RightButton; z = Qt.MiddleButton; r = False
|
||||
else:
|
||||
return
|
||||
self._buttonList =[m,z,r]
|
||||
|
||||
|
||||
####################################################
|
||||
# functions that override qt5_graphics
|
||||
####################################################
|
||||
def report_gcode_error(self, result, seq, filename):
|
||||
error_str = gcode.strerror(result)
|
||||
errortext = "G-Code error in " + os.path.basename(filename) + "\n" + "Near line " \
|
||||
+ str(seq) + " of\n" + filename + "\n" + error_str + "\n"
|
||||
STATUS.emit("graphics-gcode-error", errortext)
|
||||
|
||||
# Override qt5_graphics / glcannon.py function so we can emit a GObject signal
|
||||
# block sending out signal if the highlight request
|
||||
# came from an external source - we only send it out
|
||||
# if someone clicked on us
|
||||
def update_highlight_variable(self, line):
|
||||
self.highlight_line = line
|
||||
if self._block_line_selected:
|
||||
self._block_line_selected = False
|
||||
return
|
||||
if line is None:
|
||||
line = -1
|
||||
STATUS.emit('graphics-line-selected', line)
|
||||
|
||||
def select_fire(self):
|
||||
if self.inhibit_selection: return
|
||||
if STATUS.is_auto_running(): return
|
||||
if not self.select_primed: return
|
||||
x, y = self.select_primed
|
||||
self.select_primed = None
|
||||
self.makeCurrent()
|
||||
self.select(x, y)
|
||||
|
||||
# override user plot -One could add gl commands to plot static objects here
|
||||
def user_plot(self):
|
||||
return
|
||||
|
||||
def emit_percent(self, f):
|
||||
super( GCodeGraphics, self).emit_percent(f)
|
||||
STATUS.emit('graphics-loading-progress',f)
|
||||
|
||||
def get_joints_mode(self):
|
||||
return STATUS.stat.motion_mode == linuxcnc.TRAJ_MODE_FREE
|
||||
|
||||
def output_notify_message(self, message):
|
||||
print("Preview Notified:", message)
|
||||
mess = {}
|
||||
mess['TITLE']= "Preview Notified"
|
||||
mess["SHORTTEXT"]= "See log"
|
||||
mess['DETAILS']= message
|
||||
options ={'LOG=True,LEVEL=DEFAULT'}
|
||||
STATUS.emit('status-message', mess, options)
|
||||
|
||||
#########################################################################
|
||||
# This is how designer can interact with our widget properties.
|
||||
# property getter/setters
|
||||
#########################################################################
|
||||
|
||||
# VIEW
|
||||
def setview(self, view):
|
||||
self.current_view = view
|
||||
self.set_view(view)
|
||||
def getview(self):
|
||||
return self.current_view
|
||||
def resetview(self):
|
||||
self.set_view('p')
|
||||
_view = Property(str, getview, setview, resetview)
|
||||
|
||||
# DRO
|
||||
def setdro(self, state):
|
||||
self.setEnableDRO(state)
|
||||
def getdro(self):
|
||||
return self.getEnableDRO()
|
||||
_dro = Property(bool, getdro, setdro)
|
||||
|
||||
# DTG
|
||||
def setdtg(self, state):
|
||||
self.show_dtg = state
|
||||
self.update()
|
||||
def getdtg(self):
|
||||
return self.show_dtg
|
||||
_dtg = Property(bool, getdtg, setdtg)
|
||||
|
||||
# METRIC
|
||||
def setmetric(self, state):
|
||||
self.metric_units = state
|
||||
self.update()
|
||||
def getmetric(self):
|
||||
return self.metric_units
|
||||
_metric = Property(bool, getmetric, setmetric)
|
||||
|
||||
# overlay
|
||||
def setoverlay(self, overlay):
|
||||
self.show_overlay = overlay
|
||||
self.update()
|
||||
def getoverlay(self):
|
||||
return self.show_overlay
|
||||
def resetoverlay(self):
|
||||
self.show_overlay(False)
|
||||
_overlay = Property(bool, getoverlay, setoverlay, resetoverlay)
|
||||
|
||||
# show Offsets
|
||||
def setShowOffsets(self, state):
|
||||
self.show_offsets = state
|
||||
self.update()
|
||||
def getShowOffsets(self):
|
||||
return self.show_offsets
|
||||
_offsets = Property(bool, getShowOffsets, setShowOffsets)
|
||||
|
||||
# show small origin
|
||||
def setShowSmallOrigin(self, state):
|
||||
self.show_small_origin = state
|
||||
self.update()
|
||||
def getShowSmallOrigin(self):
|
||||
return self.show_small_origin
|
||||
_small_origin = Property(bool, getShowSmallOrigin, setShowSmallOrigin)
|
||||
|
||||
def getGridColor(self):
|
||||
return self._gridColor
|
||||
def setGridColor(self, value):
|
||||
self._gridColor = value
|
||||
self.colors['grid'] = (value.redF(), value.greenF(), value.blueF())
|
||||
self.update()
|
||||
def resetGridColor(self):
|
||||
self._gridColor = QColor(200, 138, 138)
|
||||
grid_color = Property(QColor, getGridColor, setGridColor, resetGridColor)
|
||||
|
||||
def getOverlayColor(self):
|
||||
return self._overlayColor
|
||||
def setOverlayColor(self, value):
|
||||
self._overlayColor = value
|
||||
self.colors['overlay_background'] = (value.redF(), value.greenF(), value.blueF())
|
||||
self.update()
|
||||
def resetOverlayColor(self):
|
||||
self._overlayColor = QColor(0, 0, 191, 150)
|
||||
overlay_color = Property(QColor, getOverlayColor, setOverlayColor, resetOverlayColor)
|
||||
|
||||
def getOverlayAlpha(self):
|
||||
return self._overlayAlpha
|
||||
def setOverlayAlpha(self, value):
|
||||
self._overlayAlpha = value
|
||||
self.colors['overlay_alpha'] = value
|
||||
self.update()
|
||||
def resetOverlayAlpha(self):
|
||||
self._overlayAlpha = 0.75
|
||||
overlay_alpha = Property(float, getOverlayAlpha, setOverlayAlpha, resetOverlayAlpha)
|
||||
|
||||
def getBackgroundColor(self):
|
||||
return self._backgroundColor
|
||||
def setBackgroundColor(self, value):
|
||||
self._backgroundColor = value
|
||||
#print value.getRgbF()
|
||||
self.colors['back'] = (value.redF(), value.greenF(), value.blueF())
|
||||
self.gradient_color1 = (value.redF(), value.greenF(), value.blueF())
|
||||
self.update()
|
||||
def resetBackgroundColor(self):
|
||||
self._backgroundColor = QColor(0, 0, 0, 0)
|
||||
self.gradient_color1 = QColor(0, 0, 0, 0)
|
||||
value = QColor(0, 0, 0, 0)
|
||||
self.gradient_color1 = (value.redF(), value.greenF(), value.blueF())
|
||||
self.colors['back'] = (value.redF(), value.greenF(), value.blueF())
|
||||
self.update()
|
||||
background_color = Property(QColor, getBackgroundColor, setBackgroundColor, resetBackgroundColor)
|
||||
|
||||
# use gradient background
|
||||
def setGradientBackground(self, state):
|
||||
self.use_gradient_background = state
|
||||
self.update()
|
||||
def getGradientBackground(self):
|
||||
return self.use_gradient_background
|
||||
_use_gradient_background = Property(bool, getGradientBackground, setGradientBackground)
|
||||
|
||||
def getJogColor(self):
|
||||
return self._jogColor
|
||||
def setJogColor(self, value):
|
||||
self._jogColor = value
|
||||
if value.alpha() == 0:
|
||||
c = self.get_default_plot_colors()
|
||||
self.set_plot_colors(jog = c[0])
|
||||
else:
|
||||
self.set_plot_colors(jog = (value.red(), value.green(), value.blue(),value.alpha()))
|
||||
def resetJogColor(self):
|
||||
self._jogColor = QColor(0, 0, 0, 0)
|
||||
|
||||
jog_color = Property(QColor, getJogColor, setJogColor, resetJogColor)
|
||||
|
||||
def getFeedColor(self):
|
||||
return self._feedColor
|
||||
def setFeedColor(self, value):
|
||||
self._feedColor = value
|
||||
if value.alpha() == 0:
|
||||
c = self.get_default_plot_colors()
|
||||
self.set_plot_colors(feed = c[2], arc = c[3])
|
||||
else:
|
||||
self.set_plot_colors(feed = (value.red(), value.green(), value.blue(),value.alpha()),
|
||||
arc = (value.red(), value.green(), value.blue(),value.alpha()))
|
||||
def resetFeedColor(self):
|
||||
self._feedColor = QColor(0, 0, 0, 0)
|
||||
|
||||
Feed_color = Property(QColor, getFeedColor, setFeedColor, resetFeedColor)
|
||||
|
||||
def getRapidColor(self):
|
||||
return self._rapidColor
|
||||
def setRapidColor(self, value):
|
||||
self._rapidColor = value
|
||||
if value.alpha() == 0:
|
||||
c = self.get_default_plot_colors()
|
||||
self.set_plot_colors(traverse = c[1])
|
||||
else:
|
||||
self.set_plot_colors(traverse = (value.red(), value.green(), value.blue(),value.alpha()))
|
||||
def resetRapidColor(self):
|
||||
self._rapidColor = QColor(0, 0, 0, 0)
|
||||
|
||||
Rapid_color = Property(QColor, getRapidColor, setRapidColor, resetRapidColor)
|
||||
|
||||
# Inhibit external controls
|
||||
def setInhibitControls(self, state):
|
||||
self._disable_STATUS_signals = state
|
||||
self.updateSignals(state)
|
||||
def getInhibitControls(self):
|
||||
return self._disable_STATUS_signals
|
||||
def resetInhibitControls(self):
|
||||
self._disable_STATUS_signals = False
|
||||
self.updateSignals(False)
|
||||
InhibitControls = Property(bool, getInhibitControls, setInhibitControls,resetInhibitControls)
|
||||
|
||||
# set Mouse button controls
|
||||
def setMouseButtonMode(self, value):
|
||||
self._mouseMode = value
|
||||
self.updateMouseMode(value)
|
||||
def getMouseButtonMode(self):
|
||||
return self._mouseMode
|
||||
def resetMouseButtonMode(self):
|
||||
self._mouseMode = 0
|
||||
self.updateMouseMode(0)
|
||||
MouseButtonMode = Property(int, getMouseButtonMode, setMouseButtonMode,resetMouseButtonMode)
|
||||
|
||||
# set mouse wheel zoom inversion
|
||||
def setMouseWheelInvertZoom(self, state):
|
||||
self._invertWheelZoom = state
|
||||
def getMouseWheelInvertZoom(self):
|
||||
return self._invertWheelZoom
|
||||
def resetMouseWheelInvertZoom(self):
|
||||
self._invertWheelZoom = False
|
||||
MouseWheelInvertZoom = Property(bool, getMouseWheelInvertZoom, setMouseWheelInvertZoom, resetMouseWheelInvertZoom)
|
||||
|
||||
# For testing purposes, include code to allow a widget to be created and shown
|
||||
# if this file is run.
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = GCodeGraphics()
|
||||
widget.use_gradient_background = True
|
||||
widget.enable_dro = True
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
559
lib/python/qtvcp/widgets/geditor.py
Normal file
@@ -0,0 +1,559 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Gcode display / edit widget for QT_VCP
|
||||
# Copyright 2016 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.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
# This was based on
|
||||
# QScintilla sample with PyQt
|
||||
# Eli Bendersky (eliben@gmail.com)
|
||||
# Which is code in the public domain
|
||||
#
|
||||
# See also:
|
||||
# http://pyqt.sourceforge.net/Docs/QScintilla2/index.html
|
||||
# https://qscintilla.com/
|
||||
# https://qscintilla.com/simple-example/
|
||||
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import Property, Signal, QSize, QEvent, Qt, QByteArray
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QMainWindow, QAction,\
|
||||
QToolBar, QLineEdit, QHBoxLayout, QMessageBox, \
|
||||
QFrame, QLabel
|
||||
|
||||
from qtvcp.widgets.gcode_editor import GcodeDisplay
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Action
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# INFO holds INI file details
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
#############################################
|
||||
# For Editing Gcode
|
||||
#############################################
|
||||
|
||||
class GEditor(QMainWindow, _HalWidgetBase):
|
||||
percentDone = Signal(int)
|
||||
|
||||
def __init__(self, parent=None, designer=False):
|
||||
if not designer:
|
||||
parent = None
|
||||
super().__init__(parent)
|
||||
self._show_editor = True
|
||||
self.load_dialog_code = 'LOAD'
|
||||
self.save_dialog_code = 'SAVE'
|
||||
self.dialog_code = 'KEYBOARD'
|
||||
|
||||
STATUS.connect('general',self.returnFromDialog)
|
||||
|
||||
self.isCaseSensitive = 0
|
||||
|
||||
self.setMinimumSize(QSize(300, 200))
|
||||
self.setWindowTitle("PyQt5 editor test example")
|
||||
|
||||
# make editor
|
||||
self.editor = GcodeDisplay(self)
|
||||
self.setCentralWidget(self.editor)
|
||||
|
||||
# class patch editor's function to ours
|
||||
# so we get the lines percent update
|
||||
self.editor.emit_percent = self.emit_percent
|
||||
|
||||
self.editor.setReadOnly(True)
|
||||
self.editor.setModified(False)
|
||||
|
||||
self.createActions()
|
||||
|
||||
# Create toolbar and add action
|
||||
self.toolBar = QToolBar(self)
|
||||
self.toolBar.setObjectName('{}_toolbarfile'.format( self.objectName()))
|
||||
self.toolBar.addAction(self.newAction)
|
||||
self.toolBar.addAction(self.openAction)
|
||||
self.toolBar.addAction(self.saveAction)
|
||||
self.toolBar.addAction(self.exitAction)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolBar)
|
||||
|
||||
#self.toolBar.addSeparator()
|
||||
self.toolBarLexer = QToolBar('Lexer')
|
||||
self.toolBarLexer.setObjectName('{}_toolbarlexer'.format( self.objectName()))
|
||||
|
||||
# add lexer actions
|
||||
self.toolBarLexer.addAction(self.gCodeLexerAction)
|
||||
self.toolBarLexer.addAction(self.pythonLexerAction)
|
||||
|
||||
self.toolBarLexer.addSeparator()
|
||||
self.label = QLabel('''<html><head/><body><p><span style=" font-size:20pt;
|
||||
font-weight:600;">Edit Mode</span></p></body></html>''')
|
||||
self.toolBarLexer.addWidget(self.label)
|
||||
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolBarLexer)
|
||||
|
||||
# Create toolbar and add action
|
||||
self.toolBarEdit = QToolBar('Edit')
|
||||
self.toolBarEdit.setObjectName('{}_toolbaredit'.format( self.objectName()))
|
||||
|
||||
self.toolBarEdit.addAction(self.undoAction)
|
||||
self.toolBarEdit.addAction(self.redoAction)
|
||||
self.toolBarEdit.addSeparator()
|
||||
self.toolBarEdit.addAction(self.replaceAction)
|
||||
self.toolBarEdit.addAction(self.findAction)
|
||||
self.toolBarEdit.addAction(self.previousAction)
|
||||
self.toolBarEdit.addSeparator()
|
||||
self.toolBarEdit.addAction(self.caseAction)
|
||||
self.addToolBar(Qt.BottomToolBarArea, self.toolBarEdit)
|
||||
|
||||
self.toolBarEntry = QToolBar('entry')
|
||||
self.toolBarEntry.setObjectName('{}_toolbarentry'.format( self.objectName()))
|
||||
|
||||
frame = QFrame()
|
||||
box = QHBoxLayout()
|
||||
box.addWidget(self.searchText)
|
||||
box.addWidget(self.replaceText)
|
||||
box.addStretch(1)
|
||||
frame.setLayout(box)
|
||||
self.toolBarEntry.addWidget(frame)
|
||||
self.addToolBar(Qt.BottomToolBarArea, self.toolBarEntry)
|
||||
self.readOnlyMode(save = False)
|
||||
|
||||
def createActions(self):
|
||||
# Create new action
|
||||
self.newAction = QAction(QIcon.fromTheme('document-new'), 'New', self)
|
||||
self.newAction.setShortcut('Ctrl+N')
|
||||
self.newAction.setStatusTip('New document')
|
||||
self.newAction.triggered.connect(self.newCall)
|
||||
|
||||
# Create open action
|
||||
self.openAction = QAction(QIcon.fromTheme('document-open'), '&Open', self)
|
||||
self.openAction.setShortcut('Ctrl+O')
|
||||
self.openAction.setStatusTip('Open document')
|
||||
self.openAction.triggered.connect(self.openCall)
|
||||
|
||||
# Create save action
|
||||
self.saveAction = QAction(QIcon.fromTheme('document-save'), '&Save', self)
|
||||
self.saveAction.setShortcut('Ctrl+S')
|
||||
self.saveAction.setStatusTip('Save document')
|
||||
self.saveAction.triggered.connect(self.saveCall)
|
||||
|
||||
# Create exit action
|
||||
self.exitAction = QAction(QIcon.fromTheme('application-exit'), '&Exit', self)
|
||||
self.exitAction.setShortcut('Ctrl+Q')
|
||||
self.exitAction.setToolTip('Exit Edit Mode')
|
||||
self.exitAction.setStatusTip('Exit Edit Mode')
|
||||
self.exitAction.triggered.connect(self.exitCall)
|
||||
|
||||
# Create gcode lexer action
|
||||
self.gCodeLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Gcode\nLexer', self)
|
||||
self.gCodeLexerAction.setCheckable(1)
|
||||
self.gCodeLexerAction.setShortcut('Ctrl+G')
|
||||
self.gCodeLexerAction.setStatusTip('Set Gcode highlighting')
|
||||
self.gCodeLexerAction.triggered.connect(self.gcodeLexerCall)
|
||||
|
||||
# Create gcode lexer action
|
||||
self.pythonLexerAction = QAction(QIcon.fromTheme('lexer.png'), '&Python\nLexer', self)
|
||||
self.pythonLexerAction.setShortcut('Ctrl+P')
|
||||
self.pythonLexerAction.setStatusTip('Set Python highlighting')
|
||||
self.pythonLexerAction.triggered.connect(self.pythonLexerCall)
|
||||
|
||||
self.searchText = QLineEdit(self)
|
||||
self.searchText.setToolTip('Search Text')
|
||||
self.searchText.setStatusTip('Text to search for')
|
||||
self.searchText.installEventFilter(self)
|
||||
|
||||
self.replaceText = QLineEdit(self)
|
||||
self.replaceText.setToolTip('Replace Text')
|
||||
self.replaceText.setStatusTip('Replace search text with this text')
|
||||
self.replaceText.installEventFilter(self)
|
||||
|
||||
# Create new action
|
||||
self.undoAction = QAction(QIcon.fromTheme('edit-undo'), 'Undo', self)
|
||||
self.undoAction.setStatusTip('Undo')
|
||||
self.undoAction.triggered.connect(self.undoCall)
|
||||
|
||||
# create redo action
|
||||
self.redoAction = QAction(QIcon.fromTheme('edit-redo'), 'Redo', self)
|
||||
self.redoAction.setStatusTip('Redo')
|
||||
self.redoAction.triggered.connect(self.redoCall)
|
||||
|
||||
# create replace action
|
||||
self.replaceAction = QAction(QIcon.fromTheme('edit-find-replace'), 'Replace', self)
|
||||
self.replaceAction.setStatusTip('Replace text')
|
||||
self.replaceAction.triggered.connect(self.replaceCall)
|
||||
|
||||
# create find action
|
||||
self.findAction = QAction(QIcon.fromTheme('edit-find'), 'Find', self)
|
||||
self.findAction.setStatusTip('Find next occurrence of text')
|
||||
self.findAction.triggered.connect(self.findCall)
|
||||
|
||||
# create next action
|
||||
self.previousAction = QAction(QIcon.fromTheme('go-previous'), 'Find Previous', self)
|
||||
self.previousAction.setStatusTip('Find previous occurrence of text')
|
||||
self.previousAction.triggered.connect(self.previousCall)
|
||||
|
||||
# create case action
|
||||
self.caseAction = QAction(QIcon.fromTheme('edit-case'), 'Aa', self)
|
||||
self.caseAction.setToolTip('Toggle Match Case')
|
||||
self.caseAction.setStatusTip('Toggle between any case and match case')
|
||||
self.caseAction.setCheckable(1)
|
||||
self.caseAction.triggered.connect(self.caseCall)
|
||||
|
||||
# catch focusIn event to pop keyboard dialog
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.FocusIn:
|
||||
if isinstance(obj, QLineEdit):
|
||||
# only if mouse selected
|
||||
if event.reason () == 0:
|
||||
self.popEntry(obj)
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
# callback functions built for easy class patching ##########
|
||||
# want to refrain from renaming these functions as it will break
|
||||
# any class patch user's use
|
||||
# we split them like this so a user can intercept the callback
|
||||
# but still call the original functionality
|
||||
|
||||
def caseCall(self):
|
||||
self.case()
|
||||
def case(self):
|
||||
self.isCaseSensitive -=1
|
||||
self.isCaseSensitive *=-1
|
||||
|
||||
def exitCall(self):
|
||||
self.exit()
|
||||
def exit(self):
|
||||
if self.editor.isModified():
|
||||
result = self.killCheck()
|
||||
if result:
|
||||
try:
|
||||
self.editor.reload_last(None)
|
||||
except Exception as e:
|
||||
print (e)
|
||||
self.readOnlyMode()
|
||||
return result
|
||||
return True
|
||||
|
||||
def findCall(self):
|
||||
self.find()
|
||||
def find(self):
|
||||
self.editor.search(str(self.searchText.text()),
|
||||
re=False, case=self.isCaseSensitive,
|
||||
word=False, wrap= True, fwd=True)
|
||||
|
||||
def previousCall(self):
|
||||
self.previous()
|
||||
def previous(self):
|
||||
self.editor.setCursorPosition(self.editor.getSelection()[0],
|
||||
self.editor.getSelection()[1])
|
||||
self.editor.search(str(self.searchText.text()),
|
||||
re=False, case=self.isCaseSensitive,
|
||||
word=False, wrap=True, fwd=False)
|
||||
|
||||
def gcodeLexerCall(self):
|
||||
self.gcodeLexer()
|
||||
def gcodeLexer(self):
|
||||
self.editor.set_lexer("g-code")
|
||||
|
||||
def nextCall(self):
|
||||
self.next()
|
||||
def next(self):
|
||||
self.editor.search(str(self.searchText.text()),
|
||||
re=False, case=self.isCaseSensitive,
|
||||
word=False, wrap=True, fwd=False)
|
||||
self.editor.search_Next()
|
||||
|
||||
def newCall(self):
|
||||
self.new()
|
||||
def new(self):
|
||||
if self.editor.isModified():
|
||||
result = self.killCheck()
|
||||
if result:
|
||||
self.editor.new_text()
|
||||
else:
|
||||
self.editor.new_text()
|
||||
|
||||
def openCall(self):
|
||||
self.open()
|
||||
def open(self):
|
||||
self.getFileName()
|
||||
def openReturn(self,f):
|
||||
ACTION.OPEN_PROGRAM(f)
|
||||
self.editor.setModified(False)
|
||||
|
||||
def pythonLexerCall(self):
|
||||
self.pythonLexer()
|
||||
def pythonLexer(self):
|
||||
self.editor.set_lexer("python")
|
||||
|
||||
def redoCall(self):
|
||||
self.redo()
|
||||
def redo(self):
|
||||
self.editor.redo()
|
||||
|
||||
def replaceCall(self):
|
||||
self.replace()
|
||||
def replace(self):
|
||||
self.editor.replace_text(str(self.replaceText.text()))
|
||||
self.editor.search(str(self.searchText.text()),
|
||||
re=False, case=self.isCaseSensitive,
|
||||
word=False, wrap=True, fwd=True)
|
||||
|
||||
def saveCall(self):
|
||||
self.save()
|
||||
def save(self):
|
||||
self.getSaveFileName()
|
||||
def saveReturn(self, fname):
|
||||
saved = ACTION.SAVE_PROGRAM(self.editor.text(), fname)
|
||||
if saved is not None:
|
||||
self.editor.setModified(False)
|
||||
ACTION.OPEN_PROGRAM(saved)
|
||||
|
||||
def undoCall(self):
|
||||
self.undo()
|
||||
def undo(self):
|
||||
self.editor.undo()
|
||||
|
||||
# helper functions ############################################
|
||||
|
||||
def saveSettings(self):
|
||||
self.SETTINGS_.beginGroup("geditor-{}".format(self.objectName()))
|
||||
self.SETTINGS_.setValue('state', self.saveState().data())
|
||||
self.SETTINGS_.endGroup()
|
||||
|
||||
def restoreSettings(self):
|
||||
# set recorded toolbar settings
|
||||
self.SETTINGS_.beginGroup("geditor-{}".format(self.objectName()))
|
||||
state = self.SETTINGS_.value('state')
|
||||
self.SETTINGS_.endGroup()
|
||||
if not state is None:
|
||||
try:
|
||||
self.restoreState(QByteArray(state))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def editMode(self):
|
||||
self.editor.setReadOnly(False)
|
||||
result = self.restoreSettings()
|
||||
check = (self.toolBar.toggleViewAction().isChecked() and
|
||||
self.toolBarEdit.toggleViewAction().isChecked() and
|
||||
self.toolBarEntry.toggleViewAction().isChecked() and
|
||||
self.toolBarLexer.toggleViewAction().isChecked())
|
||||
if not check:
|
||||
self.toolBar.toggleViewAction().setChecked(False)
|
||||
self.toolBar.toggleViewAction().trigger()
|
||||
if not result:
|
||||
self.toolBarEdit.toggleViewAction().setChecked(False)
|
||||
self.toolBarEdit.toggleViewAction().trigger()
|
||||
self.toolBarEntry.toggleViewAction().setChecked(False)
|
||||
self.toolBarEntry.toggleViewAction().trigger()
|
||||
self.toolBarLexer.toggleViewAction().setChecked(False)
|
||||
self.toolBarLexer.toggleViewAction().trigger()
|
||||
|
||||
def readOnlyMode(self, save = True):
|
||||
if save:
|
||||
self.saveSettings()
|
||||
self.editor.setReadOnly(True)
|
||||
self.toolBar.toggleViewAction().setChecked(True)
|
||||
self.toolBar.toggleViewAction().trigger()
|
||||
self.toolBarEdit.toggleViewAction().setChecked(True)
|
||||
self.toolBarEdit.toggleViewAction().trigger()
|
||||
self.toolBarEntry.toggleViewAction().setChecked(True)
|
||||
self.toolBarEntry.toggleViewAction().trigger()
|
||||
self.toolBarLexer.toggleViewAction().setChecked(True)
|
||||
self.toolBarLexer.toggleViewAction().trigger()
|
||||
|
||||
|
||||
def getFileName(self):
|
||||
mess = {'NAME':self.load_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'Load Editor'}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
def getSaveFileName(self):
|
||||
mess = {'NAME':self.save_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'Save Editor'}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
|
||||
def popEntry(self, obj):
|
||||
mess = {'NAME':self.dialog_code,
|
||||
'ID':'%s__' % self.objectName(),
|
||||
'OVERLAY':False,
|
||||
'OBJECT':obj,
|
||||
'TITLE':'Set Entry for {}'.format(obj.objectName().upper()),
|
||||
'GEONAME':'_{}'.format(self.dialog_code)
|
||||
}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def returnFromDialog(self, w, message):
|
||||
if message.get('NAME') == self.load_dialog_code:
|
||||
path = message.get('RETURN')
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
if path and code:
|
||||
self.openReturn(path)
|
||||
elif message.get('NAME') == self.save_dialog_code:
|
||||
path = message.get('RETURN')
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
if path and code:
|
||||
self.saveReturn(path)
|
||||
elif message.get('NAME') == self.dialog_code:
|
||||
txt = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
if code and txt is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
obj = message.get('OBJECT')
|
||||
obj.setText(str(txt))
|
||||
|
||||
def killCheck(self):
|
||||
choice = QMessageBox.question(self, 'Warning!',
|
||||
"""This file has changed since loading and
|
||||
has not been saved. You will lose your changes.
|
||||
Still want to proceed?""",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if choice == QMessageBox.Yes:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def emit_percent(self, percent):
|
||||
self.percentDone.emit(percent)
|
||||
|
||||
def select_lineup(self):
|
||||
self.editor.select_lineup(None)
|
||||
|
||||
def select_linedown(self):
|
||||
self.editor.select_linedown(None)
|
||||
|
||||
def select_line(self, line):
|
||||
self.editor.highlight_line(None, line)
|
||||
|
||||
def jump_line(self, jump):
|
||||
self.editor.jump_line(jump)
|
||||
|
||||
def get_line(self):
|
||||
return self.editor.getCursorPosition()[0] +1
|
||||
|
||||
def set_margin_width(self,width):
|
||||
self.editor.set_margin_width(width)
|
||||
|
||||
def set_font(self, font):
|
||||
self.editor.font = font
|
||||
for i in range(0,8):
|
||||
self.editor.lexer.setFont(font,i)
|
||||
|
||||
def set_background_color(self, color):
|
||||
self.editor.set_background_color(color)
|
||||
|
||||
def isReadOnly(self):
|
||||
return self.editor.isReadOnly()
|
||||
|
||||
# designer recognized getter/setters
|
||||
|
||||
# auto_show_mdi status
|
||||
# These adjust the self.editor instance
|
||||
def set_auto_show_mdi(self, data):
|
||||
self.editor.auto_show_mdi = data
|
||||
def get_auto_show_mdi(self):
|
||||
return self.editor.auto_show_mdi
|
||||
def reset_auto_show_mdi(self):
|
||||
self.editor.auto_show_mdi = True
|
||||
auto_show_mdi_status = Property(bool, get_auto_show_mdi, set_auto_show_mdi, reset_auto_show_mdi)
|
||||
|
||||
# designer recognized getter/setters
|
||||
# auto_show_manual status
|
||||
def set_auto_show_manual(self, data):
|
||||
self.editor.auto_show_manual = data
|
||||
def get_auto_show_manual(self):
|
||||
return self.editor.auto_show_manual
|
||||
def reset_auto_show_manual(self):
|
||||
self.editor.auto_show_manual = True
|
||||
auto_show_manual_status = Property(bool, get_auto_show_manual, set_auto_show_manual, reset_auto_show_manual)
|
||||
|
||||
# designer recognized getter/setters
|
||||
# show_editor status
|
||||
def set_show_editor(self, data):
|
||||
self._show_editor = data
|
||||
if data:
|
||||
self.toolBar.show()
|
||||
self.toolBarLexer.show()
|
||||
self.toolBarEntry.show()
|
||||
self.toolBarEdit.show()
|
||||
else:
|
||||
self.toolBar.hide()
|
||||
self.toolBarLexer.hide()
|
||||
self.toolBarEntry.hide()
|
||||
self.toolBarEdit.hide()
|
||||
def get_show_editor(self):
|
||||
return self._show_editor
|
||||
def reset_show_editor(self):
|
||||
self._show_editor = True
|
||||
self.toolBar.show()
|
||||
self.toolBarLexer.show()
|
||||
self.toolBarEntry.show()
|
||||
self.toolBarEdit.show()
|
||||
show_editor = Property(bool, get_show_editor, set_show_editor, reset_show_editor)
|
||||
|
||||
|
||||
# for direct testing
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = GcodeEditor()
|
||||
|
||||
w.editMode()
|
||||
w.editor.setText(''' This is test text
|
||||
a
|
||||
a
|
||||
a
|
||||
B
|
||||
b
|
||||
n
|
||||
C
|
||||
C
|
||||
C
|
||||
|
||||
This is the end of the test text.''')
|
||||
if 0:
|
||||
w.toolBar.hide()
|
||||
if 0:
|
||||
w.pythonLexerAction.setVisible(False)
|
||||
w.gCodeLexerAction.setVisible(False)
|
||||
if 0:
|
||||
w.openAction.setVisible(False)
|
||||
w.newAction.setVisible(False)
|
||||
if 0:
|
||||
w.saveAction.setVisible(False)
|
||||
w.exitAction.setVisible(False)
|
||||
if 0:
|
||||
w.label.setText('<b>Edit mode title label</b>')
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
|
||||
125
lib/python/qtvcp/widgets/general_hal_input.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtpy.QtCore import Signal, Property
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
################################################################
|
||||
class GeneralHALInput(QWidget, _HalWidgetBase):
|
||||
hal_pin_changed = Signal([int], [float], [bool])
|
||||
def __init__(self, parent=None):
|
||||
super(GeneralHALInput, self).__init__(parent)
|
||||
self.istate = False
|
||||
self._pin_name = ''
|
||||
self._bit_pin_type = True
|
||||
self._s32_pin_type = False
|
||||
self._float_pin_type = False
|
||||
self._float_scale = 1.0
|
||||
self._s32_scale = 1
|
||||
|
||||
def _hal_init(self):
|
||||
if self._pin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name
|
||||
if self._bit_pin_type:
|
||||
ptype = hal.HAL_BIT
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(self.HAL_NAME_, hal.HAL_BIT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda data: self.pin_update(data))
|
||||
elif self._float_pin_type:
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(self.HAL_NAME_, hal.HAL_FLOAT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda data: self.pin_update(data))
|
||||
else:
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(self.HAL_NAME_, hal.HAL_S32, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda data: self.pin_update(data))
|
||||
|
||||
def pin_update(self, *a):
|
||||
self.hal_pin_changed.emit(self.hal_pin.get())
|
||||
|
||||
|
||||
#########################################################################
|
||||
# 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 = ''
|
||||
|
||||
# designer will show these properties in this order:
|
||||
pin_name = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
bit_pin_type = Property(bool, get_bit_pin_type, set_bit_pin_type, reset_bit_pin_type)
|
||||
s32_pin_type = Property(bool, get_s32_pin_type, set_s32_pin_type, reset_s32_pin_type)
|
||||
float_pin_type = Property(bool, get_float_pin_type, set_float_pin_type, reset_float_pin_type)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
168
lib/python/qtvcp/widgets/general_hal_output.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
import hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtpy.QtCore import Slot, Property
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
################################################################
|
||||
class GeneralHALOutput(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(GeneralHALOutput, self).__init__(parent)
|
||||
self.istate = False
|
||||
self._initial_float = 1.0
|
||||
self._pin_name = ''
|
||||
self._bit_pin_type = True
|
||||
self._s32_float_pin_type = False
|
||||
self._float_scale = 1.0
|
||||
self._s32_scale = 1
|
||||
|
||||
def _hal_init(self):
|
||||
if self._pin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name
|
||||
if self._bit_pin_type:
|
||||
ptype = hal.HAL_BIT
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, ptype, hal.HAL_OUT)
|
||||
self.hal_pin_2 = self.HAL_GCOMP_.newpin(pname + "-not", ptype, hal.HAL_OUT)
|
||||
if self.istate:
|
||||
self._pin_bit_update(self._initial_float)
|
||||
else:
|
||||
ptype = hal.HAL_S32
|
||||
ptype2 = hal.HAL_FLOAT
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname + '-s32', ptype, hal.HAL_OUT)
|
||||
self.hal_pin_2 = self.HAL_GCOMP_.newpin(pname + "-float", ptype2, hal.HAL_OUT)
|
||||
if self.istate:
|
||||
self._pin_value_update(self._initial_float)
|
||||
|
||||
def _pin_bit_update(self, state):
|
||||
self.hal_pin.set(bool(state))
|
||||
self.hal_pin_2.set(not bool(state))
|
||||
|
||||
def _pin_value_update(self, state):
|
||||
self.hal_pin.set(state * self._s32_scale)
|
||||
self.hal_pin_2.set(state * self._float_scale)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
|
||||
@Slot(bool)
|
||||
@Slot(int)
|
||||
def pin_bit_update(self, value):
|
||||
self._pin_bit_update(value)
|
||||
|
||||
@Slot(int)
|
||||
@Slot(float)
|
||||
def pin_value_update(self, value):
|
||||
self._pin_value_update(value)
|
||||
|
||||
@Slot()
|
||||
def set_pin_true(self):
|
||||
self._pin_bit_update(1)
|
||||
@Slot()
|
||||
def set_pin_false(self):
|
||||
self._pin_bit_update(0)
|
||||
|
||||
def set_istate(self, value):
|
||||
self.istate = value
|
||||
def get_istate(self):
|
||||
return self.istate
|
||||
def reset_istate(self):
|
||||
self.istate = 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 = True
|
||||
|
||||
def set_s32_float_pin_type(self, value):
|
||||
self._s32_float_pin_type = value
|
||||
if value:
|
||||
self._toggle_properties('s32_float')
|
||||
def get_s32_float_pin_type(self):
|
||||
return self._s32_float_pin_type
|
||||
|
||||
def set_float_scale(self, data):
|
||||
self._float_scale = data
|
||||
def get_float_scale(self):
|
||||
return self._float_scale
|
||||
def reset_float_scale(self):
|
||||
self._float_scale = 1.0
|
||||
|
||||
def set_initial_float(self, data):
|
||||
self._initial_float = data
|
||||
def get_initial_float(self):
|
||||
return self._initial_float
|
||||
def reset_initial_float(self):
|
||||
self._initial_float = 1.0
|
||||
|
||||
def set_s32_scale(self, data):
|
||||
self._s32_scale = data
|
||||
def get_s32_scale(self):
|
||||
return self._s32_scale
|
||||
def reset_s32_scale(self):
|
||||
self._s32_scale = 1
|
||||
|
||||
# designer will show these properties in this order:
|
||||
initial_state = Property(bool, get_istate, set_istate, reset_istate)
|
||||
initial_float = Property(float, get_initial_float, set_initial_float, reset_initial_float)
|
||||
pin_name = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
bit_pin_type = Property(bool, get_bit_pin_type, set_bit_pin_type, reset_bit_pin_type)
|
||||
s32_float_pin_type = Property(bool, get_s32_float_pin_type, set_s32_float_pin_type)
|
||||
float_scale = Property(float, get_float_scale, set_float_scale, reset_float_scale)
|
||||
s32_scale = Property(int, get_s32_scale, set_s32_scale, reset_s32_scale)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
179
lib/python/qtvcp/widgets/hal_label.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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 hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtpy.QtCore import Property, Slot
|
||||
from qtvcp.widgets.simple_widgets import ScaledLabel
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
################################################################
|
||||
class HALLabel(ScaledLabel, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(HALLabel, self).__init__(parent)
|
||||
self._textTemplate = '%f'
|
||||
self.istate = False
|
||||
self._pin_name = ''
|
||||
self._bit_pin_type = True
|
||||
self._s32_pin_type = False
|
||||
self._float_pin_type = False
|
||||
self._use_multi_label = False
|
||||
self._multi_label_list = ['Label 0','Label 1','Label 2']
|
||||
|
||||
def _hal_init(self):
|
||||
super()._hal_init()
|
||||
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._setText(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._setText(data))
|
||||
elif self._s32_pin_type or self._use_multi_label:
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_S32, hal.HAL_IN)
|
||||
if self._s32_pin_type:
|
||||
self.hal_pin.value_changed.connect(lambda data: self._setText(data))
|
||||
else:
|
||||
self.hal_pin.value_changed.connect(lambda data: self._changeText(data))
|
||||
|
||||
# display formatted text
|
||||
def _setText(self, data):
|
||||
try:
|
||||
tmpl = lambda s: str(self._textTemplate) % s
|
||||
self.setText(tmpl(data))
|
||||
except Exception as e:
|
||||
LOG.warning('Widget "{}" format error: {}'.format(self.objectName(),e))
|
||||
|
||||
# select text from a list
|
||||
def _changeText(self, data):
|
||||
if data < 0 or data >= len(self._multi_label_list):
|
||||
self.setText('')
|
||||
else:
|
||||
self._setText(self._multi_label_list[data])
|
||||
|
||||
# one can connect signals to this widget to
|
||||
# feed an input that gets formatted by this widget.
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
@Slot(bool)
|
||||
def setDisplay(self, data):
|
||||
self._setText(data)
|
||||
|
||||
@Slot(int)
|
||||
def selectLabel(self,data):
|
||||
self._changeText(data)
|
||||
|
||||
#########################################################################
|
||||
# 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_pin_type', 's32_pin_type',
|
||||
'float_pin_type','use_multi_label')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i] = 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_pin_type')
|
||||
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_pin_type')
|
||||
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_pin_type')
|
||||
def get_float_pin_type(self):
|
||||
return self._float_pin_type
|
||||
def reset_float_pin_type(self):
|
||||
self._float_pin_type = ''
|
||||
|
||||
def set_use_multi_label(self, value):
|
||||
self._use_multi_label = value
|
||||
if value:
|
||||
self._toggle_properties('use_multi_label')
|
||||
def get_use_multi_label(self):
|
||||
return self._use_multi_label
|
||||
def reset_use_multi_label(self):
|
||||
self._use_multi_label = ''
|
||||
|
||||
def set_textTemplate(self, data):
|
||||
self._textTemplate = data
|
||||
def get_textTemplate(self):
|
||||
return self._textTemplate
|
||||
def reset_textTemplate(self):
|
||||
self._textTemplate = '%d'
|
||||
|
||||
def set_multi_label_l(self, data):
|
||||
self._multi_label_list = data
|
||||
def get_multi_label_l(self):
|
||||
return self._multi_label_list
|
||||
def reset_multi_label_l(self):
|
||||
self._multi_label_list = ['Label 0','Label 1','Label 2']
|
||||
|
||||
# designer will show these properties in this order:
|
||||
pin_name = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
bit_pin_type = Property(bool, get_bit_pin_type, set_bit_pin_type, reset_bit_pin_type)
|
||||
s32_pin_type = Property(bool, get_s32_pin_type, set_s32_pin_type, reset_s32_pin_type)
|
||||
float_pin_type = Property(bool, get_float_pin_type, set_float_pin_type, reset_float_pin_type)
|
||||
use_multi_label = Property(bool, get_use_multi_label, set_use_multi_label, reset_use_multi_label)
|
||||
textTemplate = Property(str, get_textTemplate, set_textTemplate, reset_textTemplate)
|
||||
|
||||
multi_label_list = Property('QStringList',
|
||||
get_multi_label_l, set_multi_label_l, reset_multi_label_l)
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
239
lib/python/qtvcp/widgets/hal_selectionbox.py
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import QComboBox, QTreeView, QFrame
|
||||
from qtpy.QtGui import QStandardItemModel, QStandardItem
|
||||
from qtpy.QtCore import Qt, QEvent, QModelIndex, Signal
|
||||
import hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class TreeComboBox(QComboBox):
|
||||
selectionUpdated = Signal('PyQt_PyObject')
|
||||
objectSelected = Signal('PyQt_PyObject')
|
||||
NAME = 1
|
||||
SELECTABLE = 2
|
||||
OBJECT = 3
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(TreeComboBox, self).__init__(parent)
|
||||
|
||||
tree_view = QTreeView(self)
|
||||
tree_view.setHeaderHidden( False )
|
||||
tree_view.setFrameShape(QFrame.NoFrame)
|
||||
tree_view.setEditTriggers(tree_view.NoEditTriggers)
|
||||
tree_view.setAlternatingRowColors(True)
|
||||
tree_view.setSelectionBehavior(tree_view.SelectItems)
|
||||
tree_view.setWordWrap(True)
|
||||
tree_view.setAllColumnsShowFocus(True)
|
||||
tree_view.setFixedWidth(300)
|
||||
self.setView(tree_view)
|
||||
|
||||
#self.view().viewport().installEventFilter(self)
|
||||
self.currentIndexChanged.connect(self.selected)
|
||||
|
||||
def showPopup(self):
|
||||
self.setRootModelIndex(QModelIndex())
|
||||
super(TreeComboBox, self).showPopup()
|
||||
|
||||
# set combox view the same as tree view ?
|
||||
def hidePopup(self):
|
||||
self.setRootModelIndex(self.view().currentIndex().parent())
|
||||
self.setCurrentIndex(self.view().currentIndex().row())
|
||||
super(TreeComboBox, self).hidePopup()
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
if event.type() == QEvent.MouseButtonPress and object is self.view().viewport():
|
||||
index = self.view().indexAt(event.pos())
|
||||
# print index.parent(),index.row(),index.column(),index.data(),index.data(Qt.UserRole + 1)
|
||||
# print self.view().isExpanded(self.view().currentIndex())
|
||||
# if self.itemAt(event.pos()) is None
|
||||
self.__skip_next_hide = not self.view().visualRect(index).contains(event.pos())
|
||||
return False
|
||||
|
||||
def addItems(self, parent, elements):
|
||||
for text, value, children in elements:
|
||||
item = QStandardItem(text)
|
||||
# don't let title lines be selectable
|
||||
if not value[1]:
|
||||
item.setFlags(item.flags() & -(1<<1))
|
||||
item.setData(value[0], role=Qt.ToolTipRole)
|
||||
# store the HAL name and selectability in Qt user roles
|
||||
item.setData(value[0], role=Qt.UserRole + self.NAME)
|
||||
# selectability in Qt user roles
|
||||
item.setData(value[1], role=Qt.UserRole + self.SELECTABLE)
|
||||
# all info of object
|
||||
item.setData(value[2], role=Qt.UserRole + self.OBJECT)
|
||||
parent.appendRow(item)
|
||||
|
||||
# next level
|
||||
if children:
|
||||
self.addItems(item, children)
|
||||
|
||||
# emit the (by default) signal name
|
||||
def getSelectionData(self, index,userIndex=1):
|
||||
choice = self.itemData(self.currentIndex(), Qt.UserRole + userIndex)
|
||||
return choice
|
||||
|
||||
def selected(self,index):
|
||||
if self.getSelectionData(index) is None:
|
||||
#print ('should reset selection or expand node')
|
||||
self.selectionUpdated.emit(None)
|
||||
self.objectSelected.emit({})
|
||||
self.selectionUpdated.emit(self.getSelectionData(index))
|
||||
self.objectSelected.emit(self.getSelectionData(index, self.OBJECT))
|
||||
|
||||
################################################################
|
||||
class HALSelectionBox(TreeComboBox, _HalWidgetBase):
|
||||
HAL_BIT = hal.HAL_BIT
|
||||
HAL_FLOAT = hal.HAL_FLOAT
|
||||
HAL_S32 = hal.HAL_S32
|
||||
HAL_U32 = hal.HAL_U32
|
||||
|
||||
HAL_IN = hal.HAL_IN
|
||||
HAL_OUT = hal.HAL_OUT
|
||||
HAL_IO = hal.HAL_IO
|
||||
HAL_RO = hal.HAL_RO
|
||||
HAL_RW = hal.HAL_RW
|
||||
|
||||
PINS = 1
|
||||
SIGS = SIGNALS = 2
|
||||
PARAMS = PARAMETERS = 4
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(HALSelectionBox, self).__init__(parent)
|
||||
self.PINTYPE = [self.HAL_BIT,self.HAL_FLOAT,self.HAL_S32,self.HAL_U32]
|
||||
self.PINDIRECTION = [self.HAL_IN, self.HAL_OUT]
|
||||
self.SIGTYPE = [self.HAL_BIT,self.HAL_FLOAT,self.HAL_S32,self.HAL_U32]
|
||||
self.SIGDRIVEN = [True, False]
|
||||
self.PARAMTYPE = [self.HAL_RO, self.HAL_RW]
|
||||
self.SHOWTYPES = [self.PINS]
|
||||
|
||||
def _hal_init(self):
|
||||
model = QStandardItemModel()
|
||||
model.setHorizontalHeaderLabels(['Connect to:'])
|
||||
node_pin = []
|
||||
node_pinin = []
|
||||
node_pinout = []
|
||||
node_pininout = []
|
||||
node_sig = []
|
||||
node_param = []
|
||||
parent_node = []
|
||||
if self.PINS in self.SHOWTYPES:
|
||||
# append((label -> 'Name',
|
||||
# user data -> [HAL name -> None, selectable -> False],
|
||||
# next level -> next_node_list))
|
||||
parent_node.append(('Pins', [None, False, {}], node_pin))
|
||||
node_pin.append(('IN', [None, False, {}], node_pinin))
|
||||
node_pin.append(('OUT', [None, False, {}], node_pinout))
|
||||
node_pin.append(('IO', [None, False, {}], node_pininout))
|
||||
for i in hal.get_info_pins():
|
||||
i['OBJECT'] = 'pin'
|
||||
if i['TYPE'] in self.PINTYPE:
|
||||
if i['DIRECTION'] in self.PINDIRECTION:
|
||||
if i['DIRECTION'] == self.HAL_IN:
|
||||
node_pinin.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
elif i['DIRECTION'] == self.HAL_OUT:
|
||||
node_pinout.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
else:
|
||||
node_pininout.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
|
||||
if self.SIGS in self.SHOWTYPES:
|
||||
node_sigdriven = []
|
||||
node_sigundriven = []
|
||||
parent_node.append(('Signals', [None, None, {}], node_sig))
|
||||
node_sig.append(('Driven', [None, False, {}], node_sigdriven))
|
||||
node_sig.append(('Undriven', [None, False, {}], node_sigundriven))
|
||||
|
||||
for i in hal.get_info_signals():
|
||||
i['OBJECT'] = 'signal'
|
||||
if i['TYPE'] in self.SIGTYPE:
|
||||
if not bool(i['DRIVER']is None) and bool(True) == self.SIGDRIVEN[0]:
|
||||
node_sigdriven.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
elif bool(i['DRIVER']is None) and bool(True) == self.SIGDRIVEN[1]:
|
||||
node_sigundriven.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
|
||||
if self.PARAMS in self.SHOWTYPES:
|
||||
parent_node.append(('Parameters', [None, None, {}], node_param))
|
||||
for i in hal.get_info_params():
|
||||
i['OBJECT'] = 'parameter'
|
||||
if i['TYPE'] in self.PARAMTYPE:
|
||||
node_sig.append((i['NAME'], [i['NAME'], True,i], []))
|
||||
|
||||
self.addItems(model, parent_node)
|
||||
self.setModel(model)
|
||||
self.view().setMinimumHeight(300)
|
||||
self.view().adjustSize()
|
||||
|
||||
def setShowTypes(self, types):
|
||||
''' Sets the pin type: HAL_BIT,HAL_FLOAT,HAL_S32,HAL_U32
|
||||
that will be shown in the combbox.
|
||||
widget defaults to all pin types shown
|
||||
ie. combobox.setShowTypes([combobox.HAL_BIT])'''
|
||||
self.SHOWTYPES = types
|
||||
|
||||
def setPinTypes(self, \
|
||||
types = [HAL_BIT,HAL_FLOAT,HAL_S32,HAL_U32], \
|
||||
direction = [HAL_IN, HAL_OUT]):
|
||||
''' Sets the pin type: HAL_BIT,HAL_FLOAT,HAL_S32,HAL_U32
|
||||
and direction: HAL_IN, HAL_OUT
|
||||
that will be shown in combobox.
|
||||
widget defaults to all pins, all directions.
|
||||
ie. combobox.setPinTypes([combobox.HAL_BIT], direction = [HAL_IN])'''
|
||||
self.PINTYPE = types
|
||||
self.PINDIRECTION = direction
|
||||
|
||||
def setSignalTypes(self, \
|
||||
types = [HAL_BIT,HAL_FLOAT,HAL_S32,HAL_U32], \
|
||||
driven = [True, False]):
|
||||
''' Sets the pin type: HAL_BIT,HAL_FLOAT,HAL_S32,HAL_U32
|
||||
and what type of signals (driven by a pin, not or both).
|
||||
True being driven and False being un-driven signals
|
||||
combobox.setSignalTypes([combobox.HAL_BIT], direction = [True,False])'''
|
||||
self.SIGTYPE = types
|
||||
self.SIGDRIVEN = driven
|
||||
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
x = hal.component('_X_')
|
||||
widget = HALSelectionBox()
|
||||
widget._hal_init()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
452
lib/python/qtvcp/widgets/image_switcher.py
Normal file
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2018 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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 os
|
||||
|
||||
from qtpy.QtCore import Signal, Property
|
||||
from qtpy.QtWidgets import QLabel
|
||||
from qtpy.QtGui import QPixmap
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Tool, Path
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
TOOL = Tool()
|
||||
PATH = Path()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
if PATH.IMAGEDIR is not None:
|
||||
DEFAULTIMAGE = (os.path.join(PATH.IMAGEDIR,'applet-critical.png')) or ''
|
||||
else:
|
||||
DEFAULTIMAGE = ''
|
||||
|
||||
class ImageSwitcher(QLabel, _HalWidgetBase):
|
||||
widgetChanged = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ImageSwitcher, self).__init__(parent)
|
||||
self._defaultImage = DEFAULTIMAGE
|
||||
self._imagePath = [DEFAULTIMAGE]
|
||||
self._current_number = 0
|
||||
#self.setScaledContents(True)
|
||||
self.defaultPixmap = QPixmap()
|
||||
|
||||
def _hal_init(self):
|
||||
self.show_image_by_number(self._current_number)
|
||||
|
||||
def _designerInit(self):
|
||||
self._hal_init()
|
||||
|
||||
# Show the widgets based on a reference number
|
||||
def show_image_by_number(self, number):
|
||||
#print (self.objectName(),len(self._imagePath),number)
|
||||
try:
|
||||
# outside limits or list
|
||||
if number <0 or number > len(self._imagePath)-1:
|
||||
LOG.debug('Path reference number out of range: {}'.format(number))
|
||||
self.setPixmap(self.defaultPixmap)
|
||||
return
|
||||
# specifically ask for None
|
||||
if self._imagePath[number].upper() == 'NONE':
|
||||
self.setPixmap(QPixmap())
|
||||
return
|
||||
# specifically ask for Default image
|
||||
if self._imagePath[number].upper() == 'DEFAULT':
|
||||
self.setPixmap(self.defaultPixmap)
|
||||
return
|
||||
# resources file images.
|
||||
if ':/' in self._imagePath[number]:
|
||||
path = self._imagePath[number]
|
||||
pixmap = QPixmap(path)
|
||||
# no valid image
|
||||
if pixmap.isNull():
|
||||
self.setPixmap(self.defaultPixmap)
|
||||
return
|
||||
self.setPixmap(pixmap)
|
||||
return
|
||||
else:
|
||||
path = os.path.expanduser(self._imagePath[number])
|
||||
except Exception as e:
|
||||
LOG.error('Path reference number: {}'.format(e))
|
||||
self.setPixmap(self.defaultPixmap)
|
||||
return
|
||||
path = os.path.expanduser(self.defaultPixmap)
|
||||
#print 'requested:',number,self._imagePath[number]
|
||||
# if path doesn't exist try referencing
|
||||
# from the built in image folder
|
||||
if not os.path.exists(path):
|
||||
path = os.path.join(PATH.IMAGEDIR, path)
|
||||
if not os.path.exists(path):
|
||||
LOG.debug('No Path: {}'.format(path))
|
||||
pixmap = QPixmap(path)
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
def set_default_image(self, path):
|
||||
self._defaultImage = path
|
||||
|
||||
def set_image_number(self, data):
|
||||
self._current_number = data
|
||||
self.show_image_by_number(data)
|
||||
def get_image_number(self):
|
||||
return self._current_number
|
||||
def reset_image_number(self):
|
||||
self._current_number = 0
|
||||
image_number = Property(int, get_image_number, set_image_number, reset_image_number)
|
||||
|
||||
def set_image_l(self, data):
|
||||
self._imagePath = data
|
||||
def get_image_l(self):
|
||||
return self._imagePath
|
||||
def reset_image_l(self):
|
||||
self._imagePath = [self._defaultImage]
|
||||
image_list = Property('QStringList', get_image_l, set_image_l, reset_image_l)
|
||||
|
||||
def setImagePath(self, data):
|
||||
self.defaultPixmap = data
|
||||
def getImagePath(self):
|
||||
return self.defaultPixmap
|
||||
def resetImagePath(self):
|
||||
self.defaultPixmap
|
||||
|
||||
image_default = Property(QPixmap, getImagePath, setImagePath, resetImagePath)
|
||||
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
|
||||
|
||||
class StatusImageSwitcher(ImageSwitcher):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(StatusImageSwitcher, self).__init__(parent)
|
||||
self._imagePath = [os.path.join(PATH.IMAGEDIR,'applet-critical.png'),
|
||||
os.path.join(PATH.IMAGEDIR,'spindle_ccw.gif'),
|
||||
os.path.join(PATH.IMAGEDIR,'spindle_cw.gif')]
|
||||
self.spindle = True
|
||||
self.all_homed = False
|
||||
self.axis_homed = False
|
||||
self.hard_limits = False
|
||||
self.machine_state = False
|
||||
self.command_state = False
|
||||
self.feedmode_state = False
|
||||
self.spindlemode_state = False
|
||||
self.toolnumber_state = False
|
||||
self.tool_orientation_state = False
|
||||
|
||||
self._last_limit = []
|
||||
self.axis = 'X'
|
||||
for i in range(0,len(INFO.AVAILABLE_JOINTS)):
|
||||
self._last_limit.append([0,0])
|
||||
|
||||
def _hal_init(self):
|
||||
if self.spindle:
|
||||
STATUS.connect('spindle-control-changed', lambda w, a, b, c, d: self.switch_on_spindle(b,c,d))
|
||||
elif self.all_homed:
|
||||
STATUS.connect('not-all-homed', lambda w, data: self.switch_on_homed(0))
|
||||
STATUS.connect('all-homed', lambda w: self.switch_on_homed(1))
|
||||
elif self.axis_homed:
|
||||
STATUS.connect('unhomed', lambda w, data: self.switch_on_axis_unhomed(data))
|
||||
STATUS.connect('homed', lambda w, data: self.switch_on_axis_homed(data))
|
||||
elif self.hard_limits:
|
||||
STATUS.connect('hard-limits-tripped', lambda w, data, group: self.switch_on_hard_limits(data, group))
|
||||
elif self.machine_state:
|
||||
STATUS.connect('state-estop', lambda w: self.switch_on_machine_state(0))
|
||||
STATUS.connect('interp-run', lambda w: self.switch_on_machine_state(1))
|
||||
STATUS.connect('interp-idle', lambda w: self.switch_on_machine_state(2))
|
||||
STATUS.connect('interp-paused', lambda w: self.switch_on_machine_state(3))
|
||||
STATUS.connect('interp-waiting', lambda w: self.switch_on_machine_state(4))
|
||||
STATUS.connect('interp-reading', lambda w: self.switch_on_machine_state(5))
|
||||
elif self.command_state:
|
||||
STATUS.connect('command-running', lambda w: self.switch_on_command_state(0))
|
||||
STATUS.connect('command-stopped', lambda w: self.switch_on_command_state(1))
|
||||
STATUS.connect('command-error', lambda w: self.switch_on_command_state(2))
|
||||
elif self.feedmode_state:
|
||||
STATUS.connect('fpm-mode', lambda w, d: self.switch_on_feedmode_state(0, d))
|
||||
STATUS.connect('fpr-mode', lambda w, d: self.switch_on_feedmode_state(1,d))
|
||||
STATUS.connect('itime-mode', lambda w, d: self.switch_on_feedmode_state(2,d))
|
||||
elif self.spindlemode_state:
|
||||
STATUS.connect('rpm-mode', lambda w, d: self.switch_on_spindlemode_state(0, d))
|
||||
STATUS.connect('css-mode', lambda w, d: self.switch_on_spindlemode_state(1, d))
|
||||
elif self.toolnumber_state:
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w, d:self.switch_on_toolnumber_state(d))
|
||||
elif self.tool_orientation_state:
|
||||
STATUS.connect('tool-info-changed', lambda w, d:self.switch_on_toolinfo_state(d, TOOL.ORIENTATION))
|
||||
|
||||
|
||||
def _designerInit(self):
|
||||
self.show_image_by_number(0)
|
||||
|
||||
# M5 disabled = image 0
|
||||
# M3 at speed = image 1
|
||||
# M4 at speed = image 2
|
||||
# M3 accelerating = image 3
|
||||
# M4 accelerating = image 4
|
||||
def switch_on_spindle(self, on, speed, up):
|
||||
if not on:
|
||||
num = 0
|
||||
elif not up:
|
||||
if speed<0: num = 4
|
||||
else: num = 3
|
||||
elif speed <0: num = 2
|
||||
else: num = 1
|
||||
self.set_image_number(num)
|
||||
|
||||
def switch_on_homed(self, data):
|
||||
if not data <0:
|
||||
self.set_image_number(data)
|
||||
|
||||
def switch_on_axis_homed(self, num):
|
||||
data = INFO.GET_NAME_FROM_JOINT[int(num)]
|
||||
if data == self.axis:
|
||||
self.set_image_number(1)
|
||||
|
||||
def switch_on_axis_unhomed(self, num):
|
||||
data = INFO.GET_NAME_FROM_JOINT[int(num)]
|
||||
if data == self.axis:
|
||||
self.set_image_number(0)
|
||||
|
||||
def switch_on_hard_limits(self, data, group):
|
||||
if not data:
|
||||
# tripped
|
||||
self.set_image_number(0)
|
||||
elif (len(self._imagePath)) == 2:
|
||||
#print 'bool images'
|
||||
self.set_image_number(1)
|
||||
elif (len(self._imagePath)-1) == (len(INFO.AVAILABLE_JOINTS)):
|
||||
#print 'per joint limits images', self._last_limit, group
|
||||
for i in range(0,len(INFO.AVAILABLE_JOINTS)):
|
||||
if group[i] == self._last_limit[i]:
|
||||
pass
|
||||
elif group[i] == [0,0]:
|
||||
pass
|
||||
else:
|
||||
self.set_image_number(i+1)
|
||||
break
|
||||
elif (len(self._imagePath)-1) == (len(INFO.AVAILABLE_JOINTS) * 2):
|
||||
pass
|
||||
#print 'per joint and per end limits images'
|
||||
self._last_limit = group
|
||||
|
||||
def switch_on_machine_state(self, state):
|
||||
self.set_image_number(state)
|
||||
|
||||
def switch_on_command_state(self, state):
|
||||
self.set_image_number(state)
|
||||
|
||||
def switch_on_feedmode_state(self, mode, state):
|
||||
if state:
|
||||
self.set_image_number(mode)
|
||||
|
||||
def switch_on_spindlemode_state(self, mode, state):
|
||||
if state:
|
||||
self.set_image_number(mode)
|
||||
|
||||
def switch_on_toolnumber_state(self, number):
|
||||
self.set_image_number(number)
|
||||
|
||||
def switch_on_toolinfo_state(self, tool_entry, index):
|
||||
toolnum = tool_entry[0]
|
||||
tool_table_line = TOOL.GET_TOOL_INFO(toolnum)
|
||||
try:
|
||||
self.set_image_number(tool_table_line[index])
|
||||
except Exception as e:
|
||||
LOG.warning('Problem with tool file info')
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('spindle','all_homed', 'axis_homed','hard_limits',
|
||||
'machine_state', 'command_state', 'feedmode_state',
|
||||
'spindlemode_state', 'tool_orientation_state',
|
||||
'toolnumber_state' )
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self['watch_'+i] = False
|
||||
|
||||
# property getter/setters
|
||||
|
||||
# machine_spindle status
|
||||
def set_spindle(self, data):
|
||||
self.spindle = data
|
||||
if data:
|
||||
self._toggle_properties('spindle')
|
||||
def get_spindle(self):
|
||||
return self.spindle
|
||||
def reset_spindle(self):
|
||||
self.spindle = False
|
||||
watch_spindle = Property(bool, get_spindle, set_spindle, reset_spindle)
|
||||
|
||||
# machine_homed status
|
||||
def set_homed(self, data):
|
||||
self.all_homed = data
|
||||
if data:
|
||||
self._toggle_properties('all_homed')
|
||||
def get_homed(self):
|
||||
return self.all_homed
|
||||
def reset_homed(self):
|
||||
self.all_homed = False
|
||||
watch_all_homed = Property(bool, get_homed, set_homed, reset_homed)
|
||||
|
||||
# machine_axis_homed status
|
||||
def set_axis_homed(self, data):
|
||||
self.axis_homed = data
|
||||
if data:
|
||||
self._toggle_properties('axis_homed')
|
||||
def get_axis_homed(self):
|
||||
return self.axis_homed
|
||||
def reset_axis_homed(self):
|
||||
self.axis_homed = False
|
||||
watch_axis_homed = Property(bool, get_axis_homed, set_axis_homed, reset_axis_homed)
|
||||
|
||||
|
||||
# machine_limits status
|
||||
def set_limits(self, data):
|
||||
self.hard_limits = data
|
||||
if data:
|
||||
self._toggle_properties('hard_limits')
|
||||
def get_limits(self):
|
||||
return self.hard_limits
|
||||
def reset_limits(self):
|
||||
self.hard_limits = False
|
||||
watch_hard_limits = Property(bool, get_limits, set_limits, reset_limits)
|
||||
|
||||
# machine_state status
|
||||
def set_machine_state(self, data):
|
||||
self.machine_state = data
|
||||
if data:
|
||||
self._toggle_properties('machine_state')
|
||||
def get_machine_state(self):
|
||||
return self.machine_state
|
||||
def reset_machine_state(self):
|
||||
self.machine_state = False
|
||||
watch_machine_state = Property(bool, get_machine_state, set_machine_state,
|
||||
reset_machine_state)
|
||||
|
||||
# command_state status
|
||||
def set_command_state(self, data):
|
||||
self.command_state = data
|
||||
if data:
|
||||
self._toggle_properties('command_state')
|
||||
def get_command_state(self):
|
||||
return self.command_state
|
||||
def reset_command_state(self):
|
||||
self.command_state = False
|
||||
watch_command_state = Property(bool, get_command_state, set_command_state,
|
||||
reset_command_state)
|
||||
|
||||
# feedmode_state status
|
||||
def set_feedmode_state(self, data):
|
||||
self.feedmode_state = data
|
||||
if data:
|
||||
self._toggle_properties('feedmode_state')
|
||||
def get_feedmode_state(self):
|
||||
return self.feedmode_state
|
||||
def reset_feedmode_state(self):
|
||||
self.feedmode_state = False
|
||||
watch_feedmode_state = Property(bool, get_feedmode_state, set_feedmode_state,
|
||||
reset_feedmode_state)
|
||||
# spindlemode_state status
|
||||
def set_spindlemode_state(self, data):
|
||||
self.spindlemode_state = data
|
||||
if data:
|
||||
self._toggle_properties('spindlemode_state')
|
||||
def get_spindlemode_state(self):
|
||||
return self.spindlemode_state
|
||||
def reset_spindlemode_state(self):
|
||||
self.spindlemode_state = False
|
||||
watch_spindlemode_state = Property(bool, get_spindlemode_state, set_spindlemode_state,
|
||||
reset_spindlemode_state)
|
||||
|
||||
# toolnumber_state status
|
||||
def set_toolnumber_state(self, data):
|
||||
self.toolnumber_state = data
|
||||
if data:
|
||||
self._toggle_properties('toolnumber_state')
|
||||
def get_toolnumber_state(self):
|
||||
return self.toolnumber_state
|
||||
def reset_toolnumber_state(self):
|
||||
self.toolnumber_state = False
|
||||
watch_toolnumber_state = Property(bool, get_toolnumber_state, set_toolnumber_state,
|
||||
reset_toolnumber_state)
|
||||
|
||||
# tool_orientation_state status
|
||||
def set_tool_orientation_state(self, data):
|
||||
self.tool_orientation_state = data
|
||||
if data:
|
||||
self._toggle_properties('tool_orientation_state')
|
||||
def get_tool_orientation_state(self):
|
||||
return self.tool_orientation_state
|
||||
def reset_tool_orientation_state(self):
|
||||
self.tool_orientation_state = False
|
||||
watch_tool_orientation_state = Property(bool, get_tool_orientation_state, set_tool_orientation_state,
|
||||
reset_tool_orientation_state)
|
||||
|
||||
def set_axis(self, data):
|
||||
if data.upper() in('X','Y','Z','A','B','C','U','V','W'):
|
||||
self.axis = data.upper()
|
||||
def get_axis(self):
|
||||
return self.axis
|
||||
def reset_axis(self):
|
||||
self.axis = 'X'
|
||||
axis_letter = Property(str, get_axis, set_axis, reset_axis)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = ImageSwitcher()
|
||||
widget._hal_init()
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
1197
lib/python/qtvcp/widgets/indicatorMixIn.py
Normal file
220
lib/python/qtvcp/widgets/jog_increments.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
#################################################################################
|
||||
|
||||
from qtpy import QtCore, QtWidgets
|
||||
|
||||
import hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INFO is INI file details
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class JogIncrements(QtWidgets.QComboBox, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(JogIncrements, self).__init__(parent)
|
||||
self._pin_name = ''
|
||||
self.linear = True
|
||||
self._block_signal = False
|
||||
|
||||
# Default to continuous jogging (selection 0)
|
||||
# with a combo box display, it's assumed the showing increment
|
||||
# is valid - so we must update the rate if the units mode changes.
|
||||
def _hal_init(self):
|
||||
# TODO should this be optional?
|
||||
if 1==1:
|
||||
if self._pin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
if self.linear:
|
||||
for item in (INFO.JOG_INCREMENTS):
|
||||
self.addItem(item)
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('jogincrement-changed', lambda w, value, text: self._checkincrements(value, text))
|
||||
else:
|
||||
for item in (INFO.ANGULAR_INCREMENTS):
|
||||
self.addItem(item)
|
||||
STATUS.connect('jogincrement-angular-changed', lambda w, value, text: self._checkincrements(value, text))
|
||||
self.currentIndexChanged.connect(self.selectionchange)
|
||||
self.selectionchange(0)
|
||||
|
||||
# if units switch - current jog rate in invalid
|
||||
def _switch_units(self, w, data):
|
||||
if self.currentIndex() > 0:
|
||||
self.selectionchange(-1)
|
||||
|
||||
# search the combo box for the value STATUS sent us
|
||||
# If there is a match, change the combobox to it
|
||||
# otherwise display a blank in the combobox.
|
||||
# in this way if some other widget sets a rate change
|
||||
# the combo box doesn't lie
|
||||
def _checkincrements(self,value, text):
|
||||
self.hal_pin.set(value)
|
||||
for count in range(self.count()):
|
||||
label = self.itemText(count)
|
||||
try:
|
||||
number = float(label.rstrip(" inchmuildeg").lower())
|
||||
except:
|
||||
number = None
|
||||
# assume continuous jogging (selection 0)
|
||||
if number is None:
|
||||
machn_incr = 0
|
||||
else:
|
||||
if self.linear:
|
||||
machn_incr = self.parse_increment(label)
|
||||
else:
|
||||
machn_incr = self.parse_angular_increment(label)
|
||||
#print count,self.itemText(count), machn_incr,value
|
||||
if machn_incr is not None and round(machn_incr,6) == round(value,6):
|
||||
self._block_signal = True
|
||||
self.setCurrentIndex(count)
|
||||
self._block_signal = False
|
||||
return
|
||||
self.setCurrentIndex(-1)
|
||||
|
||||
def selectionchange(self, i):
|
||||
if self._block_signal: return
|
||||
text = str(self.currentText())
|
||||
if i == -1 and text == '': return
|
||||
try:
|
||||
# continuous jog (selection 0)
|
||||
if i == 0:
|
||||
inc = 0
|
||||
else:
|
||||
if self.linear:
|
||||
inc = self.parse_increment(text)
|
||||
else:
|
||||
inc = self.parse_angular_increment(text)
|
||||
|
||||
except Exception as e:
|
||||
LOG.debug('Exception parsing increment - setting increment to None', exc_info=e)
|
||||
inc = None
|
||||
if inc is None:
|
||||
LOG.warning("parsed: text not recognized : {} Increment: {}".format(text, inc))
|
||||
return
|
||||
if self.linear:
|
||||
LOG.debug("Linear Current index: {} Increment: {} , selection changed {}".format(i, inc, text))
|
||||
STATUS.set_jog_increments(inc, text)
|
||||
else:
|
||||
LOG.debug("Angular Current index: {} Increment: {} , selection changed {}".format(i, inc, text))
|
||||
STATUS.set_jog_increment_angular(inc, text)
|
||||
|
||||
def parse_angular_increment(self, jogincr):
|
||||
scale = 1
|
||||
if jogincr.endswith("deg"):
|
||||
scale = 1
|
||||
incr = jogincr.rstrip(" deg")
|
||||
incr = float(incr)
|
||||
return incr * scale
|
||||
|
||||
# We convert INI parced increments to machine units
|
||||
def parse_increment(self, jogincr):
|
||||
if jogincr.endswith("mm"):
|
||||
scale = self.conversion(1)
|
||||
elif jogincr.endswith("cm"):
|
||||
scale = self.conversion(10)
|
||||
elif jogincr.endswith("um"):
|
||||
scale = self.conversion(.001)
|
||||
elif jogincr.endswith("in") or jogincr.endswith("inch"):
|
||||
scale = self.conversion(1., metric = False)
|
||||
elif jogincr.endswith("mil"):
|
||||
scale = self.conversion(.001, metric = False)
|
||||
else:
|
||||
scale = 1
|
||||
incr = jogincr.rstrip(" inchmuil")
|
||||
try:
|
||||
if "/" in incr:
|
||||
p, q = incr.split("/")
|
||||
incr = float(p) / float(q)
|
||||
else:
|
||||
incr = float(incr)
|
||||
except:
|
||||
return None
|
||||
LOG.debug("parsed: text: {} Increment: {} scaled: {}".format(jogincr, incr, (incr * scale)))
|
||||
return incr * scale
|
||||
|
||||
# This does the conversion
|
||||
# calling function must tell us if the data is metric or not.
|
||||
def conversion(self, data, metric = True):
|
||||
if STATUS.is_metric_mode():
|
||||
if metric:
|
||||
return INFO.convert_metric_to_machine(data)
|
||||
else:
|
||||
return INFO.convert_imperial_to_machine(data)
|
||||
else:
|
||||
if metric:
|
||||
return INFO.convert_metric_to_machine(data)
|
||||
else:
|
||||
return INFO.convert_imperial_to_machine(data)
|
||||
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('linear')
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i+'_option'] = False
|
||||
|
||||
# BOOL VARIABLES----------------------
|
||||
def set_linear(self, data):
|
||||
self.linear = data
|
||||
def get_linear(self):
|
||||
return self.linear
|
||||
def reset_linear(self):
|
||||
self.linear = True
|
||||
|
||||
# designer will show these properties in this order:
|
||||
# BOOL
|
||||
linear_option = QtCore.Property(bool, get_linear, set_linear, reset_linear)
|
||||
|
||||
# VARIABLES---------------------------
|
||||
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 = ''
|
||||
|
||||
pinName = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
combo = JogIncrements()
|
||||
combo.show()
|
||||
sys.exit(app.exec())
|
||||
681
lib/python/qtvcp/widgets/joypad.py
Normal file
@@ -0,0 +1,681 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp Joypad widget
|
||||
#
|
||||
# Copyright (c) 2021 Jim Sloot <persei802@gmail.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
|
||||
# (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 sys
|
||||
import enum
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from qtpy.QtCore import Qt, QPoint, QPointF, QRect, QRectF, QSize, QSizeF, QEvent
|
||||
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 qtpy.QtGui import QPainter, QPainterPath, QPen, QBrush, QColor, QFont, QPixmap, QRadialGradient
|
||||
|
||||
import hal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
|
||||
class IndicatorPosition(enum.IntEnum):
|
||||
NONE = 0
|
||||
LEFT = 1
|
||||
RIGHT = 2
|
||||
CENTER = 3
|
||||
TOP = 4
|
||||
BOTTOM = 5
|
||||
LEFTRIGHT = 6
|
||||
TOPBOTTOM = 7
|
||||
|
||||
class JoyPad(QtWidgets.QWidget):
|
||||
IndicatorPosition = IndicatorPosition
|
||||
QEnum(IndicatorPosition)
|
||||
|
||||
joy_btn_pressed = QtCore.Signal(str)
|
||||
joy_btn_released = QtCore.Signal(str)
|
||||
joy_l_pressed = QtCore.Signal(bool)
|
||||
joy_l_released = QtCore.Signal(bool)
|
||||
joy_r_pressed = QtCore.Signal(bool)
|
||||
joy_r_released = QtCore.Signal(bool)
|
||||
joy_c_pressed = QtCore.Signal(bool)
|
||||
joy_c_released = QtCore.Signal(bool)
|
||||
joy_t_pressed = QtCore.Signal(bool)
|
||||
joy_t_released = QtCore.Signal(bool)
|
||||
joy_b_pressed = QtCore.Signal(bool)
|
||||
joy_b_released = QtCore.Signal(bool)
|
||||
def __init__(self, parent=None):
|
||||
super(JoyPad, self).__init__(parent)
|
||||
self.rect1 = QRectF()
|
||||
self.rect2 = QRectF()
|
||||
self.left_image = None
|
||||
self.right_image = None
|
||||
self.top_image = None
|
||||
self.bottom_image = None
|
||||
self.center_image = None
|
||||
self._dummyPixmap = QtGui.QPixmap()
|
||||
self._font = QFont('Lato Heavy', 20)
|
||||
self._text_color = QColor('white')
|
||||
self._textL = ''
|
||||
self._textR = ''
|
||||
self._textC = ''
|
||||
self._textT = ''
|
||||
self._textB = ''
|
||||
self.colorState = False
|
||||
self._true_color = QColor('lawngreen')
|
||||
self._false_color = QColor('gray')
|
||||
self.highlight_color = self._false_color
|
||||
self._highlightPosition = IndicatorPosition.NONE
|
||||
self.highlight_left = False
|
||||
self.highlight_right = False
|
||||
self.highlight_top = False
|
||||
self.highlight_bottom = False
|
||||
self.highlight_center = False
|
||||
self.last_active_btn = None
|
||||
self.setMouseTracking(True)
|
||||
self.setToolTipDuration(2000)
|
||||
self.installEventFilter(self)
|
||||
self.btn_names = {'L': 'left', 'R': 'right', 'T': 'top', 'B': 'bottom', 'C': 'center'}
|
||||
self.tooltips = {'L': '', 'R': '', 'T': '', 'B': '', 'C': ''}
|
||||
self.axis_list = ('X', 'Y', 'Z', 'A')
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if obj is self and self.isEnabled():
|
||||
if event.type() == QEvent.MouseButtonPress:
|
||||
if event.button() == Qt.RightButton:
|
||||
event.ignore()
|
||||
else:
|
||||
pos = event.localPos()
|
||||
active_btn = self.get_active_btn(pos)
|
||||
self.last_active_btn = active_btn
|
||||
if active_btn is not None:
|
||||
self._pressedOutput(active_btn)
|
||||
elif event.type() == QEvent.MouseButtonRelease:
|
||||
if event.button() == Qt.RightButton:
|
||||
event.ignore()
|
||||
elif self.last_active_btn is not None:
|
||||
self._releasedOutput(self.last_active_btn)
|
||||
elif event.type() == QEvent.MouseMove:
|
||||
pos = event.pos()
|
||||
active_btn = self.get_active_btn(pos)
|
||||
if active_btn is not None:
|
||||
self.setToolTip(self.tooltips[active_btn])
|
||||
return super(JoyPad, self).eventFilter(obj, event)
|
||||
|
||||
def _pressedOutput(self, btncode):
|
||||
self.joy_btn_pressed.emit(btncode)
|
||||
self['joy_{}_pressed'.format(btncode.lower())].emit(True)
|
||||
|
||||
def _releasedOutput(self, btncode):
|
||||
self.joy_btn_released.emit(btncode)
|
||||
self['joy_{}_released'.format(btncode.lower())].emit(False)
|
||||
|
||||
def get_active_btn(self, pos):
|
||||
if self.center_path.contains(pos): return 'C'
|
||||
elif self.left_path.contains(pos): return 'L'
|
||||
elif self.right_path.contains(pos): return 'R'
|
||||
elif self.bottom_path.contains(pos): return 'B'
|
||||
elif self.top_path.contains(pos): return 'T'
|
||||
return None
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(painter.Antialiasing)
|
||||
w = min(event.rect().width(), event.rect().height())
|
||||
self.rect1.setSize(QSizeF(w * 0.4, w * 0.4))
|
||||
self.rect2.setSize(QSizeF(w * 0.9, w * 0.9))
|
||||
self.create_paths(painter, event)
|
||||
self.draw_painter_paths(painter, event)
|
||||
self.draw_icons(painter, event)
|
||||
self.draw_highlight(painter, event)
|
||||
painter.end()
|
||||
|
||||
def create_paths(self, qp, event):
|
||||
self.left_path = QPainterPath()
|
||||
self.right_path = QPainterPath()
|
||||
self.bottom_path = QPainterPath()
|
||||
self.top_path = QPainterPath()
|
||||
self.center_path = QPainterPath()
|
||||
center = event.rect().center()
|
||||
self.rect1.moveCenter(center)
|
||||
self.rect2.moveCenter(center)
|
||||
left_start = QPointF(self.rect1.topLeft())
|
||||
right_start = QPointF(self.rect1.bottomRight())
|
||||
bottom_start = QPointF(self.rect1.bottomLeft())
|
||||
top_start = QPointF(self.rect1.topRight())
|
||||
path = (self.right_path, self.top_path, self.left_path, self.bottom_path)
|
||||
start = (right_start, top_start, left_start, bottom_start)
|
||||
angle = -45
|
||||
for i in range(4):
|
||||
path[i].moveTo(start[i])
|
||||
path[i].arcTo(self.rect1, angle, 90)
|
||||
path[i].arcTo(self.rect2, angle + 90, -90)
|
||||
path[i].closeSubpath()
|
||||
angle += 90
|
||||
cap = QRectF()
|
||||
cap.setSize(QSizeF(self.rect1.width()*0.8, self.rect1.height()*0.8))
|
||||
cap.moveCenter(center)
|
||||
self.center_path.addEllipse(cap)
|
||||
|
||||
def draw_painter_paths(self, qp, event):
|
||||
w = min(event.rect().width(), event.rect().height())
|
||||
center = event.rect().center()
|
||||
fp = QPoint(int(center.x() - w/4), int(center.y() - w/4))
|
||||
bg = QRadialGradient(center, w/2, fp)
|
||||
bg.setColorAt(0, QColor(180, 180, 180))
|
||||
bg.setColorAt(1, QColor(40, 40, 40))
|
||||
qp.setBrush(QBrush(bg))
|
||||
qp.setPen(QPen(QColor(Qt.black), 4))
|
||||
qp.drawPath(self.left_path)
|
||||
qp.drawPath(self.right_path)
|
||||
qp.drawPath(self.top_path)
|
||||
qp.drawPath(self.bottom_path)
|
||||
qp.drawPath(self.center_path)
|
||||
|
||||
def draw_icons(self, qp, event):
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(int(self.rect1.width() * 0.4), int(self.rect1.height() * 0.4)))
|
||||
center = event.rect().center()
|
||||
qp.setPen(QPen(self._text_color, 2))
|
||||
qp.setFont(self._font)
|
||||
# left button
|
||||
rect.moveCenter(QPoint(int(center.x() - self.rect2.width()/3), center.y()))
|
||||
if isinstance(self.left_image, QPixmap):
|
||||
pix = self.left_image
|
||||
qp.drawPixmap(rect, pix, pix.rect())
|
||||
elif isinstance(self.left_image, str):
|
||||
qp.drawText(rect, Qt.AlignCenter, self.left_image)
|
||||
# right button
|
||||
rect.moveCenter(QPoint(int(center.x() + self.rect2.width()/3), center.y()))
|
||||
if isinstance(self.right_image, QPixmap):
|
||||
pix = self.right_image
|
||||
qp.drawPixmap(rect, pix, pix.rect())
|
||||
elif isinstance(self.right_image, str):
|
||||
qp.drawText(rect, Qt.AlignCenter, self.right_image)
|
||||
# bottom button
|
||||
rect.moveCenter(QPoint(center.x(), int(center.y() + self.rect2.width()/3)))
|
||||
if isinstance(self.bottom_image, QPixmap):
|
||||
pix = self.bottom_image
|
||||
qp.drawPixmap(rect, pix, pix.rect())
|
||||
elif isinstance(self.bottom_image, str):
|
||||
qp.drawText(rect, Qt.AlignCenter, self.bottom_image)
|
||||
# top button
|
||||
rect.moveCenter(QPoint(center.x(), int(center.y() - self.rect2.width()/3)))
|
||||
if isinstance(self.top_image, QPixmap):
|
||||
pix = self.top_image
|
||||
qp.drawPixmap(rect, pix, pix.rect())
|
||||
elif isinstance(self.top_image, str):
|
||||
qp.drawText(rect, Qt.AlignCenter, self.top_image)
|
||||
# center button
|
||||
rect.moveCenter(QPoint(center.x(), center.y()))
|
||||
if isinstance(self.center_image, QPixmap):
|
||||
pix = self.center_image
|
||||
qp.drawPixmap(rect, pix, pix.rect())
|
||||
elif isinstance(self.center_image, str):
|
||||
qp.drawText(rect, Qt.AlignCenter, self.center_image)
|
||||
|
||||
def draw_highlight(self, qp, event):
|
||||
rect = QRectF()
|
||||
rect.setSize(self.rect1.size() * 0.9)
|
||||
center = event.rect().center()
|
||||
rect.moveCenter(center)
|
||||
pen_width = self.rect1.width() * 0.08
|
||||
qp.setPen(QPen(self.highlight_color, pen_width, cap = Qt.FlatCap))
|
||||
if self.highlight_center is True:
|
||||
qp.drawArc(rect, 0, 5760)
|
||||
else:
|
||||
if self.highlight_right is True:
|
||||
qp.drawArc(rect, -45 * 16, 90 * 16)
|
||||
if self.highlight_left is True:
|
||||
qp.drawArc(rect, 135 * 16, 90 * 16)
|
||||
if self.highlight_top is True:
|
||||
qp.drawArc(rect, 45 * 16, 90 * 16)
|
||||
if self.highlight_bottom is True:
|
||||
qp.drawArc(rect, 225 * 16, 90 * 16)
|
||||
|
||||
def reset_highlight(self):
|
||||
self.highlight_left = False
|
||||
self.highlight_right = False
|
||||
self.highlight_top = False
|
||||
self.highlight_bottom = False
|
||||
self.highlight_center = False
|
||||
|
||||
def set_highlight(self, btn, state=True):
|
||||
if type(btn) == int:
|
||||
if btn == IndicatorPosition.LEFT:
|
||||
btn ='L'
|
||||
elif btn == IndicatorPosition.RIGHT:
|
||||
btn ='R'
|
||||
elif btn == IndicatorPosition.CENTER:
|
||||
btn ='C'
|
||||
elif btn == IndicatorPosition.TOP:
|
||||
btn ='T'
|
||||
elif btn == IndicatorPosition.BOTTOM:
|
||||
btn ='B'
|
||||
elif btn == IndicatorPosition.LEFTRIGHT:
|
||||
btn ='X'
|
||||
elif btn == IndicatorPosition.TOPBOTTOM:
|
||||
btn ='Z'
|
||||
elif btn == IndicatorPosition.NONE:
|
||||
self.reset_highlight()
|
||||
return
|
||||
else:
|
||||
print ('undefined position:{}'.format(btn))
|
||||
return
|
||||
if btn not in self.axis_list and btn not in self.btn_names.keys(): return
|
||||
if btn == 'X' or btn == 'A':
|
||||
self.highlight_left = state
|
||||
self.highlight_right = state
|
||||
elif btn == 'Y' or btn == 'Z':
|
||||
self.highlight_top = state
|
||||
self.highlight_bottom = state
|
||||
else:
|
||||
name = self.btn_names[btn]
|
||||
self['highlight_' + name] = state
|
||||
self.update()
|
||||
|
||||
def set_button_icon(self, btn, path):
|
||||
self.set_icon(btn, 'image', path)
|
||||
def set_button_text(self, btn, text):
|
||||
self.set_icon(btn, 'text', text)
|
||||
|
||||
def set_icon(self, btn, kind, data):
|
||||
if btn not in self.btn_names.keys(): return
|
||||
name = self.btn_names[btn]
|
||||
if kind == 'image':
|
||||
if data is None:
|
||||
self[name + "_image"] = None
|
||||
else:
|
||||
self[name + "_image"] = QPixmap(data)
|
||||
elif kind == 'text':
|
||||
self[name + "_image"] = data
|
||||
else: return
|
||||
self.update()
|
||||
|
||||
def set_tooltip(self, btn, tip):
|
||||
if btn in self.btn_names.keys():
|
||||
self.tooltips[btn] = tip
|
||||
|
||||
def setLight(self, data):
|
||||
if data:
|
||||
self.highlight_color = self._true_color
|
||||
else:
|
||||
self.highlight_color = self._false_color
|
||||
self.update()
|
||||
|
||||
def set_HighlightPosition(self, position):
|
||||
self._highlightPosition = position
|
||||
self.reset_highlight()
|
||||
self.set_highlight(position, True)
|
||||
self.update()
|
||||
|
||||
def get_HighlightPosition(self):
|
||||
return self._highlightPosition
|
||||
|
||||
def reset_HighlightPosition(self):
|
||||
self._highlightPosition = IndicatorPosition.NONE
|
||||
self.reset_highlight()
|
||||
self.update()
|
||||
|
||||
highlightPosition = QtCore.Property(IndicatorPosition,
|
||||
get_HighlightPosition, set_HighlightPosition, reset_HighlightPosition)
|
||||
|
||||
@QtCore.Slot()
|
||||
def set_colorStateTrue(self):
|
||||
self.setLight(True)
|
||||
@QtCore.Slot()
|
||||
def set_colorStateFalse(self):
|
||||
self.setLight(False)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def set_colorState(self, state):
|
||||
self.colorState = bool(state)
|
||||
self.setLight(state)
|
||||
|
||||
def get_colorState(self):
|
||||
return self.colorState
|
||||
|
||||
def reset_colorState(self):
|
||||
self.colorState = False
|
||||
self.setLight(False)
|
||||
|
||||
setColorState = QtCore.Property(bool, get_colorState, set_colorState, reset_colorState)
|
||||
|
||||
def setLeftImagePath(self, data):
|
||||
if data.isNull():
|
||||
data = None
|
||||
self.set_icon('L', 'image', data)
|
||||
def getLeftImagePath(self):
|
||||
if isinstance(self.left_image, QPixmap):
|
||||
self.left_image
|
||||
else:
|
||||
return self._dummyPixmap
|
||||
def resetLeftImagePath(self):
|
||||
pass
|
||||
|
||||
def setRightImagePath(self, data):
|
||||
if data.isNull():
|
||||
data = None
|
||||
self.set_icon('R', 'image', data)
|
||||
def getRightImagePath(self):
|
||||
return self._dummyPixmap
|
||||
def resetRightImagePath(self):
|
||||
pass
|
||||
|
||||
def setCenterImagePath(self, data):
|
||||
if data.isNull():
|
||||
data = None
|
||||
self.set_icon('C', 'image', data)
|
||||
def getCenterImagePath(self):
|
||||
return self._dummyPixmap
|
||||
def resetCenterImagePath(self):
|
||||
pass
|
||||
|
||||
def setTopImagePath(self, data):
|
||||
if data.isNull():
|
||||
data = None
|
||||
self.set_icon('T', 'image', data)
|
||||
def getTopImagePath(self):
|
||||
return self._dummyPixmap
|
||||
def resetTopImagePath(self):
|
||||
pass
|
||||
|
||||
def setBottomImagePath(self, data):
|
||||
if data.isNull():
|
||||
data = None
|
||||
self.set_icon('B', 'image', data)
|
||||
def getBottomImagePath(self):
|
||||
return self._dummyPixmap
|
||||
def resetBottomImagePath(self):
|
||||
pass
|
||||
|
||||
left_image_path = QtCore.Property(QPixmap, getLeftImagePath, setLeftImagePath, resetLeftImagePath)
|
||||
right_image_path = QtCore.Property(QPixmap, getRightImagePath, setRightImagePath, resetRightImagePath)
|
||||
center_image_path = QtCore.Property(QPixmap, getCenterImagePath, setCenterImagePath, resetCenterImagePath)
|
||||
top_image_path = QtCore.Property(QPixmap, getTopImagePath, setTopImagePath, resetTopImagePath)
|
||||
bottom_image_path = QtCore.Property(QPixmap, getBottomImagePath, setBottomImagePath, resetBottomImagePath)
|
||||
|
||||
|
||||
def getFont(self):
|
||||
return self._font
|
||||
def setFont(self, value):
|
||||
self._font = value
|
||||
def resetFont(self):
|
||||
self._font = QFont('Lato Heavy', 20)
|
||||
button_font = QtCore.Property(QFont, getFont, setFont, resetFont)
|
||||
|
||||
def setLeftText(self, data):
|
||||
self._textL = data
|
||||
if not data.strip():
|
||||
data = None
|
||||
self.set_icon('L', 'text', data)
|
||||
def getLeftText(self):
|
||||
return self._textL
|
||||
def resetLeftText(self):
|
||||
self._textL = ''
|
||||
self.set_icon('L', 'text', '')
|
||||
|
||||
def setRightText(self, data):
|
||||
self._textR = data
|
||||
if not data.strip():
|
||||
data = None
|
||||
self.set_icon('R', 'text', data)
|
||||
def getRightText(self):
|
||||
return self._textR
|
||||
def resetRightText(self):
|
||||
self._textR = ''
|
||||
self.set_icon('R', 'text', '')
|
||||
|
||||
def setCenterText(self, data):
|
||||
self._textC = data
|
||||
if not data.strip():
|
||||
data = None
|
||||
self.set_icon('C', 'text', data)
|
||||
def getCenterText(self):
|
||||
return self._textC
|
||||
def resetCenterText(self):
|
||||
self._textC = ''
|
||||
self.set_icon('C', 'text', '')
|
||||
|
||||
def setTopText(self, data):
|
||||
self._textT = data
|
||||
if not data.strip():
|
||||
data = None
|
||||
self.set_icon('T', 'text', data)
|
||||
def getTopText(self):
|
||||
return self._textT
|
||||
def resetTopText(self):
|
||||
self._textT = ''
|
||||
self.set_icon('T', 'text', '')
|
||||
|
||||
def setBottomText(self, data):
|
||||
self._textB = data
|
||||
if not data.strip():
|
||||
data = None
|
||||
self.set_icon('B', 'text', data)
|
||||
def getBottomText(self):
|
||||
return self._textB
|
||||
def resetBottomText(self):
|
||||
self._textB = ''
|
||||
self.set_icon('B', 'text', '')
|
||||
|
||||
left_text = QtCore.Property(str, getLeftText, setLeftText, resetLeftText)
|
||||
right_text = QtCore.Property(str, getRightText, setRightText, resetRightText)
|
||||
center_text = QtCore.Property(str, getCenterText, setCenterText, resetCenterText)
|
||||
top_text = QtCore.Property(str, getTopText, setTopText, resetTopText)
|
||||
bottom_text = QtCore.Property(str, getBottomText, setBottomText, resetBottomText)
|
||||
|
||||
@QtCore.Slot(QColor)
|
||||
def set_true_color(self, color):
|
||||
self._true_color = color
|
||||
self.setLight(self.colorState)
|
||||
@QtCore.Slot(str)
|
||||
def set_true_color(self, color):
|
||||
self._true_color = QColor(color)
|
||||
self.setLight(self.colorState)
|
||||
|
||||
def get_true_color(self):
|
||||
return self._true_color
|
||||
|
||||
def reset_true_color(self):
|
||||
self._true_color = QColor('lawngreen')
|
||||
self.setLight(self.colorState)
|
||||
|
||||
@QtCore.Slot(QColor)
|
||||
def set_false_color(self, color):
|
||||
self._false_color = color
|
||||
self.setLight(self.colorState)
|
||||
@QtCore.Slot(str)
|
||||
def set_false_color(self, color):
|
||||
self._false_color = QColor(color)
|
||||
self.setLight(self.colorState)
|
||||
|
||||
def get_false_color(self):
|
||||
return self._false_color
|
||||
|
||||
def reset_false_color(self):
|
||||
self._false_color = QColor('gray')
|
||||
self.setLight(self.colorState)
|
||||
|
||||
def set_text_color(self, color):
|
||||
self._text_color = QColor(color)
|
||||
self.update()
|
||||
def get_text_color(self):
|
||||
return self._text_color
|
||||
def reset_text_color(self):
|
||||
self._text_color = QColor('white')
|
||||
self.update()
|
||||
|
||||
true_color = QtCore.Property(QColor, get_true_color, set_true_color, reset_true_color)
|
||||
false_color = QtCore.Property(QColor, get_false_color, set_false_color, reset_false_color)
|
||||
text_color = QtCore.Property(QColor, get_text_color, set_text_color, reset_text_color)
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def btn_pressed(self, btn):
|
||||
print("Button pressed", btn)
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def btn_released(self, btn):
|
||||
print("Button released", btn)
|
||||
|
||||
# required code for object indexing
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
class HALPinType(enum.IntEnum):
|
||||
NONE = 0
|
||||
BIT = hal.HAL_BIT
|
||||
S32 = hal.HAL_S32
|
||||
FLOAT = hal.HAL_FLOAT
|
||||
|
||||
class HALPad(JoyPad, _HalWidgetBase):
|
||||
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(HALPad, self).__init__(parent)
|
||||
self._pin_name = ''
|
||||
self._pin_type = HALPinType.BIT
|
||||
|
||||
self._trueOutputR = 1.0
|
||||
self._trueOutputL = 1.0
|
||||
self._trueOutputC = 1.0
|
||||
self._trueOutputT = 1.0
|
||||
self._trueOutputB = 1.0
|
||||
self._falseOutput = 0.0
|
||||
|
||||
self._true_color = QColor('lawngreen')
|
||||
self._false_color = QColor('red')
|
||||
|
||||
# we want center active for an indicator light
|
||||
self.setProperty('highlightPosition',IndicatorPosition.CENTER)
|
||||
self.setLight(False)
|
||||
|
||||
def _hal_init(self):
|
||||
if self._pin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name
|
||||
if not self._pin_type == HALPinType.NONE:
|
||||
ptype = self._pin_type
|
||||
self.halPinR = self.HAL_GCOMP_.newpin(pname + '.right', ptype, hal.HAL_OUT)
|
||||
self.halPinL = self.HAL_GCOMP_.newpin(pname + '.left', ptype, hal.HAL_OUT)
|
||||
self.halPinT = self.HAL_GCOMP_.newpin(pname + '.top', ptype, hal.HAL_OUT)
|
||||
self.halPinB = self.HAL_GCOMP_.newpin(pname + '.bottom', ptype, hal.HAL_OUT)
|
||||
self.halPinC = self.HAL_GCOMP_.newpin(pname + '.center', ptype, hal.HAL_OUT)
|
||||
if not self._highlightPosition == IndicatorPosition.NONE:
|
||||
self.halPinLightCenter = self.HAL_GCOMP_.newpin(pname + '.light.center', hal.HAL_BIT, hal.HAL_IN)
|
||||
self.halPinLightCenter.value_changed.connect(lambda data: self.setLight(data))
|
||||
|
||||
def _pressedOutput(self, btncode):
|
||||
self.joy_btn_pressed.emit(btncode)
|
||||
self['joy_{}_pressed'.format(btncode.lower())].emit(True)
|
||||
|
||||
if self._pin_type == HALPinType.BIT:
|
||||
data = True
|
||||
elif self._pin_type == HALPinType.FLOAT:
|
||||
data = float(self['_trueOutput{}'.format(btncode)])
|
||||
elif self._pin_type == HALPinType.S32:
|
||||
data = int(self['_trueOutput{}'.format(btncode)])
|
||||
else:
|
||||
return
|
||||
try:
|
||||
self['halPin{}'.format(btncode)].set(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _releasedOutput(self, btncode):
|
||||
self.joy_btn_released.emit(btncode)
|
||||
self['joy_{}_released'.format(btncode.lower())].emit(False)
|
||||
|
||||
if self._pin_type == HALPinType.BIT:
|
||||
data = False
|
||||
elif self._pin_type == HALPinType.FLOAT:
|
||||
data = float(self._falseOutput)
|
||||
elif self._pin_type == HALPinType.S32:
|
||||
data = int(self._falseOutput)
|
||||
else:
|
||||
return
|
||||
try:
|
||||
self['halPin{}'.format(btncode)].set(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
def setPinTrueOutput(self, btn, data):
|
||||
btncode = self.btn_names[btn.lower()]
|
||||
self['_trueOutput{}'.format(btncode)] = data
|
||||
|
||||
#########################################################################
|
||||
# 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 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_pin_type(self, value):
|
||||
self._pin_type = value
|
||||
print(value)
|
||||
def get_pin_type(self):
|
||||
return self._pin_type
|
||||
def reset_pin_type(self):
|
||||
self._pin_type = HALPinType.BIT
|
||||
|
||||
# designer will show these properties in this order:
|
||||
pin_type = QtCore.Property(HALPinType, get_pin_type, set_pin_type, reset_pin_type)
|
||||
pin_name = QtCore.Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
|
||||
#############################
|
||||
# Testing #
|
||||
#############################
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from qtpy.QtWidgets import QWidget
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = QWidget()
|
||||
w.setGeometry(100, 100, 600, 400)
|
||||
w.setWindowTitle('JoyPad')
|
||||
joy = JoyPad()
|
||||
joy.set_icon('L', 'text', 'X-')
|
||||
joy.set_icon('R', 'text', 'X+')
|
||||
joy.set_icon('T', 'text', 'Y+')
|
||||
joy.set_icon('B', 'text', 'Y-')
|
||||
joy.set_icon('C', 'image', 'stop.png')
|
||||
joy.set_tooltip('T', 'This is the top button')
|
||||
joy.set_tooltip('C', 'This is the center button')
|
||||
joy.set_false_color('red')
|
||||
joy.set_highlight('T', True)
|
||||
joy.joy_btn_pressed.connect(joy.btn_pressed)
|
||||
joy.joy_btn_released.connect(joy.btn_released)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(joy)
|
||||
w.setLayout(layout)
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
299
lib/python/qtvcp/widgets/led_widget.py
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
#################################################################################
|
||||
|
||||
from qtpy.QtCore import Property, Slot, Qt, QTimer, QSize
|
||||
from qtpy.QtGui import QColor, QPainter, QBrush, QRadialGradient
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase, hal
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Set the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class LED(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(LED, self).__init__(parent)
|
||||
self._diamX = 0
|
||||
self._diamY = 0
|
||||
self._diameter = 15
|
||||
self._color = QColor("red")
|
||||
self._off_color = QColor("black")
|
||||
self._gradient = True
|
||||
self._on_gradient_color = Qt.white
|
||||
self._off_gradient_color = Qt.white
|
||||
self._border_color = QColor("black")
|
||||
self._alignment = Qt.AlignCenter
|
||||
self.state = False
|
||||
self.flash = False
|
||||
self._state = False
|
||||
self._flashing = False
|
||||
self._flashRate = 200
|
||||
|
||||
self._timer = QTimer()
|
||||
self._timer.timeout.connect(self.toggleState)
|
||||
|
||||
self.setDiameter(self._diameter)
|
||||
self.setGradient(self._gradient)
|
||||
|
||||
self._halpin_option = True
|
||||
|
||||
def _hal_init(self):
|
||||
if (self._halpin_option):
|
||||
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_BIT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda s: self.change_state(s))
|
||||
|
||||
@Slot(bool)
|
||||
def change_state(self, data):
|
||||
self.state = data
|
||||
if data and self.flash:
|
||||
self.setFlashing(True)
|
||||
elif data and not self.flash:
|
||||
self.setState(True)
|
||||
else:
|
||||
self.setFlashing(False)
|
||||
self.setState(False)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter()
|
||||
x = 0
|
||||
y = 0
|
||||
if self._alignment & Qt.AlignLeft:
|
||||
x = 0
|
||||
elif self._alignment & Qt.AlignRight:
|
||||
x = self.width() - self._diameter
|
||||
elif self._alignment & Qt.AlignHCenter:
|
||||
x = (self.width() - self._diameter) // 2
|
||||
elif self._alignment & Qt.AlignJustify:
|
||||
x = 0
|
||||
|
||||
if self._alignment & Qt.AlignTop:
|
||||
y = 0
|
||||
elif self._alignment & Qt.AlignBottom:
|
||||
y = self.height() - self._diameter
|
||||
elif self._alignment & Qt.AlignVCenter:
|
||||
y = (self.height() - self._diameter) // 2
|
||||
|
||||
if self._gradient:
|
||||
gradient = QRadialGradient(x + self._diameter / 2, y + self._diameter / 2,
|
||||
self._diameter * 0.4, self._diameter * 0.4, self._diameter * 0.4)
|
||||
|
||||
|
||||
if self._state:
|
||||
gradient.setColorAt(0, self._on_gradient_color)
|
||||
gradient.setColorAt(1, self._color)
|
||||
else:
|
||||
gradient.setColorAt(0, self._off_gradient_color)
|
||||
gradient.setColorAt(1, self._off_color)
|
||||
|
||||
brush = QBrush(gradient)
|
||||
else:
|
||||
brush = QBrush(self._color if self._state else self._off_color)
|
||||
|
||||
painter.begin(self)
|
||||
painter.setPen(self._border_color)
|
||||
painter.setRenderHint(QPainter.Antialiasing, True)
|
||||
painter.setBrush(brush)
|
||||
painter.drawEllipse(x, y, self._diameter - 1, self._diameter - 1)
|
||||
painter.end()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return QSize(self._diameter, self._diameter)
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(self._diameter, self._diameter)
|
||||
|
||||
def getDiameter(self):
|
||||
return self._diameter
|
||||
|
||||
def set_halpin_option(self, value):
|
||||
self._halpin_option = value
|
||||
def get_halpin_option(self):
|
||||
return self._halpin_option
|
||||
def reset_halpin_option(self):
|
||||
self._halpin_option = True
|
||||
|
||||
@Slot(int)
|
||||
def setDiameter(self, value):
|
||||
self._diameter = value
|
||||
self.update()
|
||||
|
||||
def getColor(self):
|
||||
return self._color
|
||||
|
||||
@Slot(QColor)
|
||||
def setColor(self, value):
|
||||
self._color = value
|
||||
self.update()
|
||||
|
||||
def getOffColor(self):
|
||||
return self._off_color
|
||||
|
||||
@Slot(QColor)
|
||||
def setOffColor(self, value):
|
||||
self._off_color = value
|
||||
self.update()
|
||||
|
||||
def getOnGradientColor(self):
|
||||
return self._on_gradient_color
|
||||
@Slot(QColor)
|
||||
def setOnGradientColor(self, value):
|
||||
self._on_gradient_color = value
|
||||
self.update()
|
||||
def resetOnGradientColor(self, value):
|
||||
self._on_gradient_color = Qt.white
|
||||
self.update()
|
||||
|
||||
def getOffGradientColor(self):
|
||||
return self._off_gradient_color
|
||||
@Slot(QColor)
|
||||
def setOffGradientColor(self, value):
|
||||
self._off_gradient_color = value
|
||||
self.update()
|
||||
def resetOffGradientColor(self, value):
|
||||
self._off_gradient_color = Qt.white
|
||||
self.update()
|
||||
|
||||
def getGradient(self):
|
||||
return self._gradient
|
||||
|
||||
@Slot(bool)
|
||||
def setGradient(self, value):
|
||||
self._gradient = value
|
||||
self.update()
|
||||
|
||||
def resetGradient(self):
|
||||
self._gradient = True
|
||||
|
||||
def getBorderColor(self):
|
||||
return self._border_color
|
||||
|
||||
@Slot(QColor)
|
||||
def setBorderColor(self, value):
|
||||
self._border_color = value
|
||||
self.update()
|
||||
|
||||
def getAlignment(self):
|
||||
return self._alignment
|
||||
|
||||
@Slot(object)
|
||||
def setAlignment(self, value):
|
||||
self._alignment = value
|
||||
self.update()
|
||||
|
||||
def resetAlignment(self):
|
||||
self._alignment = Qt.AlignCenter
|
||||
|
||||
@Slot(bool)
|
||||
@Slot(int)
|
||||
def setState(self, value):
|
||||
self.state = self._state = value
|
||||
self.update()
|
||||
|
||||
def getState(self):
|
||||
return self.state
|
||||
|
||||
def resetState(self):
|
||||
self.state = self._state = False
|
||||
|
||||
@Slot()
|
||||
def toggleState(self):
|
||||
self._state = not self._state
|
||||
self.update()
|
||||
|
||||
def isFlashing(self):
|
||||
return self._flashing
|
||||
|
||||
@Slot(bool)
|
||||
def setFlashing(self, value):
|
||||
self._flashing = value
|
||||
if self._flashRate > 0 and value:
|
||||
if self._timer.isActive():
|
||||
return
|
||||
self._timer.start(self._flashRate)
|
||||
else:
|
||||
self._timer.stop()
|
||||
# make sure when flashing stops led ends up at state
|
||||
self._state = self.state
|
||||
self.update()
|
||||
|
||||
# flash when state on
|
||||
def setFlashState(self, value):
|
||||
self.flash = value
|
||||
self.update()
|
||||
|
||||
def getFlashState(self):
|
||||
return self.flash
|
||||
|
||||
def getFlashRate(self):
|
||||
return self._flashRate
|
||||
|
||||
@Slot(int)
|
||||
def setFlashRate(self, value):
|
||||
self._flashRate = value
|
||||
self.update()
|
||||
|
||||
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 = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
halpin_option = Property(bool, get_halpin_option, set_halpin_option, reset_halpin_option)
|
||||
diameter = Property(int, getDiameter, setDiameter)
|
||||
gradient = Property(bool, getGradient, setGradient, resetGradient)
|
||||
color = Property(QColor, getColor, setColor)
|
||||
off_color = Property(QColor, getOffColor, setOffColor)
|
||||
off_gradient_color = Property(QColor, getOffGradientColor, setOffGradientColor, resetOffGradientColor)
|
||||
on_gradient_color = Property(QColor, getOnGradientColor, setOnGradientColor, resetOnGradientColor)
|
||||
border_color = Property(QColor, getBorderColor, setBorderColor)
|
||||
alignment = Property(int, getAlignment, setAlignment, resetAlignment)
|
||||
currentstate = Property(bool, getState, setState, resetState)
|
||||
flashing = Property(bool, getFlashState, setFlashState)
|
||||
flashRate = Property(int, getFlashRate, setFlashRate)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
led = LED()
|
||||
led.show()
|
||||
|
||||
# Flash always when method called test
|
||||
#led.setFlashing(True)
|
||||
# this shouldn't matter
|
||||
#led.setState(False)
|
||||
|
||||
# Flash only when state on test
|
||||
led.setFlashState(True)
|
||||
# only flash when this is true
|
||||
led.change_state(False)
|
||||
|
||||
sys.exit(app.exec())
|
||||
418
lib/python/qtvcp/widgets/machine_log.py
Normal file
@@ -0,0 +1,418 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget - Machine Log
|
||||
# Copyright (c) 2017 Chris Morley with additions by Steve Richardson
|
||||
#
|
||||
# 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 os
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from qtpy.QtWidgets import QWidget, QTextEdit, QTableWidget, QTableWidgetItem, QVBoxLayout
|
||||
from qtpy.QtCore import QFile, Property
|
||||
import qtpy.QtWidgets as QtWidgets
|
||||
from qtpy.QtGui import QColor
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Action
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
class MachineLog(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(MachineLog, self).__init__(parent)
|
||||
self._delay = 0
|
||||
self._hash_code = None
|
||||
self._log_on = True
|
||||
self._machine_log = True
|
||||
self._machine_log_severity = False
|
||||
self._integrator_log = False
|
||||
self._critical_fg_color = QColor(255, 255, 255)
|
||||
self._critical_bg_color = QColor(255, 0, 0)
|
||||
self._error_fg_color = QColor(255, 0, 0)
|
||||
self._error_bg_color = QColor(255,255,255)
|
||||
self._warning_fg_color = QColor(255, 255, 0)
|
||||
self._warning_bg_color = QColor(0, 0, 0)
|
||||
self._debug_fg_color = QColor(128, 128, 128)
|
||||
self._debug_bg_color = QColor(255, 255, 255)
|
||||
self._info_fg_color = QColor(0, 0, 0)
|
||||
self._info_bg_color = QColor(255, 255, 255)
|
||||
|
||||
self.integratorPath = os.path.expanduser(INFO.QTVCP_LOG_HISTORY_PATH)
|
||||
self.machineLogPath = os.path.expanduser(INFO.MACHINE_LOG_HISTORY_PATH)
|
||||
|
||||
self.logText = QTextEdit(self)
|
||||
self.logTable = QTableWidget(self)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.logText)
|
||||
layout.addWidget(self.logTable)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.logTable.setColumnCount(3)
|
||||
self.logTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.logTable.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
self.logTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.logTable.setShowGrid(False)
|
||||
self.logTable.setSortingEnabled(False)
|
||||
|
||||
horizontalHeader = self.logTable.horizontalHeader()
|
||||
horizontalHeader.resizeSection(0, 100)
|
||||
horizontalHeader.resizeSection(1, 100)
|
||||
horizontalHeader.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
|
||||
horizontalHeader.setVisible(False)
|
||||
|
||||
verticalHeader = self.logTable.verticalHeader()
|
||||
verticalHeader.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
||||
verticalHeader.setVisible(False)
|
||||
|
||||
def _hal_init(self):
|
||||
if self._machine_log:
|
||||
STATUS.connect('machine-log-changed',lambda w: self.loadLog())
|
||||
self.logTable.hide()
|
||||
self.logText.show()
|
||||
elif self._machine_log_severity:
|
||||
STATUS.connect('update-machine-log', lambda w, d, o: self.updateMachineLog(d, o))
|
||||
self.logTable.show()
|
||||
self.logText.hide()
|
||||
elif self._integrator_log:
|
||||
STATUS.connect('periodic', self._periodicCheck)
|
||||
self.logTable.hide()
|
||||
self.logText.show()
|
||||
|
||||
def _periodicCheck(self, w):
|
||||
if self._delay < 9:
|
||||
self._delay += 1
|
||||
return
|
||||
if STATUS.is_status_valid() == False:
|
||||
return
|
||||
self._delay = 0
|
||||
m1 = self.md5sum(self.integratorPath)
|
||||
if m1 and self._hash_code != m1:
|
||||
self._hash_code = m1
|
||||
self.loadIntegratorLog()
|
||||
|
||||
# create a hash code
|
||||
def md5sum(self,filename):
|
||||
try:
|
||||
f = open(filename, "rb")
|
||||
except:
|
||||
return None
|
||||
else:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
|
||||
def remove_options(self, option, to_remove):
|
||||
options = option.split(',')
|
||||
options = [opt for opt in options if opt not in to_remove]
|
||||
return ','.join(options)
|
||||
|
||||
def updateMachineLog(self, message, option):
|
||||
|
||||
if option == 'DELETE':
|
||||
self.clear()
|
||||
return
|
||||
|
||||
if option == 'ON':
|
||||
self._log_on = True
|
||||
return
|
||||
if option == 'OFF':
|
||||
self._log_on = False
|
||||
return
|
||||
if self._log_on != True:
|
||||
return
|
||||
|
||||
if message:
|
||||
if option is None: option = ''
|
||||
|
||||
if 'DATE' in option:
|
||||
dateItem = QTableWidgetItem(time.strftime("%a, %b %d %Y %X"))
|
||||
else:
|
||||
dateItem = QTableWidgetItem(time.strftime("%H:%M:%S"))
|
||||
|
||||
option = self.remove_options(option, ['DATE', 'TIME', 'INITIAL'])
|
||||
|
||||
severityItem = QTableWidgetItem(option.strip())
|
||||
|
||||
msgItem = QTableWidgetItem(message.strip())
|
||||
|
||||
if 'DEBUG' in option:
|
||||
msgItem.setForeground(self._debug_fg_color)
|
||||
dateItem.setForeground(self._debug_fg_color)
|
||||
severityItem.setForeground(self._debug_fg_color)
|
||||
msgItem.setBackground(self._debug_bg_color)
|
||||
dateItem.setBackground(self._debug_bg_color)
|
||||
severityItem.setBackground(self._debug_bg_color)
|
||||
elif 'SUCCESS' in option:
|
||||
msgItem.setForeground(QColor(0, 128, 0))
|
||||
dateItem.setForeground(QColor(0, 128, 0))
|
||||
severityItem.setForeground(QColor(0, 128, 0))
|
||||
elif 'WARNING' in option:
|
||||
msgItem.setForeground(self._warning_fg_color)
|
||||
dateItem.setForeground(self._warning_fg_color)
|
||||
severityItem.setForeground(self._warning_fg_color)
|
||||
msgItem.setBackground(self._warning_bg_color)
|
||||
dateItem.setBackground(self._warning_bg_color)
|
||||
severityItem.setBackground(self._warning_bg_color)
|
||||
if self.QTVCP_INSTANCE_.OPTIONS_.play_sounds:
|
||||
ACTION.PLAY_WARNING()
|
||||
elif 'ERROR' in option:
|
||||
msgItem.setForeground(self._error_fg_color)
|
||||
dateItem.setForeground(self._error_fg_color)
|
||||
severityItem.setForeground(self._error_fg_color)
|
||||
msgItem.setBackground(self._error_bg_color)
|
||||
dateItem.setBackground(self._error_bg_color)
|
||||
severityItem.setBackground(self._error_bg_color)
|
||||
if self.QTVCP_INSTANCE_.OPTIONS_.play_sounds:
|
||||
ACTION.PLAY_ATTENTION()
|
||||
elif 'CRITICAL' in option:
|
||||
msgItem.setForeground(self._critical_fg_color)
|
||||
dateItem.setForeground(self._critical_fg_color)
|
||||
severityItem.setForeground(self._critical_fg_color)
|
||||
msgItem.setBackground(self._critical_bg_color)
|
||||
dateItem.setBackground(self._critical_bg_color)
|
||||
severityItem.setBackground(self._critical_bg_color)
|
||||
if self.QTVCP_INSTANCE_.OPTIONS_.play_sounds:
|
||||
ACTION.PLAY_ERROR()
|
||||
else:
|
||||
msgItem.setForeground(self._info_fg_color)
|
||||
dateItem.setForeground(self._info_fg_color)
|
||||
severityItem.setForeground(self._info_fg_color)
|
||||
msgItem.setBackground(self._info_bg_color)
|
||||
dateItem.setBackground(self._info_bg_color)
|
||||
severityItem.setBackground(self._info_bg_color)
|
||||
|
||||
|
||||
self.logTable.insertRow(self.logTable.rowCount())
|
||||
self.logTable.setItem(self.logTable.rowCount()-1, 0, dateItem)
|
||||
self.logTable.setItem(self.logTable.rowCount()-1, 1, severityItem)
|
||||
self.logTable.setItem(self.logTable.rowCount()-1, 2, msgItem)
|
||||
self.logTable.scrollToBottom()
|
||||
|
||||
def loadLog(self):
|
||||
file = QFile(self.machineLogPath)
|
||||
file.open(QFile.ReadOnly)
|
||||
logText = file.readAll()
|
||||
file.close()
|
||||
self.logText.setPlainText(str(logText, encoding='utf8'))
|
||||
# scroll down to show last entry
|
||||
self.logText.verticalScrollBar().setSliderPosition(self.logText.verticalScrollBar().maximum())
|
||||
|
||||
def loadIntegratorLog(self):
|
||||
file = QFile(self.integratorPath)
|
||||
file.open(QFile.ReadOnly)
|
||||
logText = file.readAll()
|
||||
file.close()
|
||||
if str(logText, encoding='utf8') == "":
|
||||
self.logText.setPlainText('No Logging found. Is QtVcp in info, debugging or verbose mode (-i, -d or -v)?')
|
||||
return
|
||||
self.logText.setHtml(str(logText, encoding='utf8'))
|
||||
# scroll down to show last entry
|
||||
self.logText.verticalScrollBar().setSliderPosition(self.logText.verticalScrollBar().maximum())
|
||||
|
||||
def showEvent(self, ev):
|
||||
self.scrollToBottom()
|
||||
|
||||
def scrollToBottom(self):
|
||||
# scroll down to show last entry
|
||||
self.logText.verticalScrollBar().setSliderPosition(self.logText.verticalScrollBar().maximum())
|
||||
self.logTable.scrollToBottom()
|
||||
|
||||
def hideCursor(self):
|
||||
self.logText.setCursorWidth(0)
|
||||
|
||||
def getLogText(self):
|
||||
if self._machine_log_severity:
|
||||
return self.readTableData()
|
||||
else:
|
||||
return self.logText.toPlainText()
|
||||
|
||||
def clear(self):
|
||||
self.logTable.clearContents()
|
||||
self.logTable.setRowCount(0)
|
||||
self.logText.setPlainText('')
|
||||
|
||||
def readTableData(self):
|
||||
data = ''
|
||||
for row in range(self.logTable.rowCount()):
|
||||
row_data = []
|
||||
for col in range(self.logTable.columnCount()):
|
||||
item = self.logTable.item(row, col).text()
|
||||
if col == 1:
|
||||
# pad the severity column for ease of reading
|
||||
row_data.append("{:^10}".format(item))
|
||||
elif item != '':
|
||||
row_data.append(item)
|
||||
data += ' '.join(row_data)
|
||||
data +='\n'
|
||||
return data
|
||||
|
||||
################## properties ###################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('machine_log', 'machine_log_severity', 'integrator_log')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i+'_option'] = False
|
||||
|
||||
def set_machine_log(self, value):
|
||||
self._machine_log = value
|
||||
if value:
|
||||
self._toggle_properties('machine_log')
|
||||
def get_machine_log(self):
|
||||
return self._machine_log
|
||||
def reset_machine_log(self):
|
||||
self._machine_log = True
|
||||
machine_log_option = Property(bool, get_machine_log, set_machine_log, reset_machine_log)
|
||||
|
||||
def set_machine_log_severity(self, value):
|
||||
self._machine_log_severity = value
|
||||
if value:
|
||||
self._toggle_properties('machine_log_severity')
|
||||
def get_machine_log_severity(self):
|
||||
return self._machine_log_severity
|
||||
def reset_machine_log_severity(self):
|
||||
self._machine_log_severity = False
|
||||
machine_log_severity_option = Property(bool, get_machine_log_severity, set_machine_log_severity, reset_machine_log_severity)
|
||||
|
||||
|
||||
def set_integrator_log(self, value):
|
||||
self._integrator_log = value
|
||||
if value:
|
||||
self._toggle_properties('integrator_log')
|
||||
def get_integrator_log(self):
|
||||
return self._integrator_log
|
||||
def reset_integrator_log(self):
|
||||
self._integrator_log = False
|
||||
integrator_log_option = Property(bool, get_integrator_log, set_integrator_log, reset_integrator_log)
|
||||
|
||||
def set_critical_fg_color(self, value):
|
||||
self._critical_fg_color = value
|
||||
def get_critical_fg_color(self):
|
||||
return self._critical_fg_color
|
||||
def reset_critical_fg_color(self):
|
||||
self._critical_fg_color = QColor(255, 255, 255)
|
||||
|
||||
critical_fg_color_option = Property(QColor, get_critical_fg_color, set_critical_fg_color, reset_critical_fg_color)
|
||||
|
||||
def set_critical_bg_color(self, value):
|
||||
self._critical_bg_color = value
|
||||
def get_critical_bg_color(self):
|
||||
return self._critical_bg_color
|
||||
def reset_critical_bg_color(self):
|
||||
self._critical_bg_color = QColor(255, 0, 0)
|
||||
|
||||
critical_bg_color_option = Property(QColor, get_critical_bg_color, set_critical_bg_color, reset_critical_bg_color)
|
||||
|
||||
def set_error_fg_color(self, value):
|
||||
self._error_fg_color = value
|
||||
def get_error_fg_color(self):
|
||||
return self._error_fg_color
|
||||
def reset_error_fg_color(self):
|
||||
self._error_fg_color = QColor(255, 0, 0)
|
||||
|
||||
error_fg_color_option = Property(QColor, get_error_fg_color, set_error_fg_color, reset_error_fg_color)
|
||||
|
||||
def set_error_bg_color(self, value):
|
||||
self._error_bg_color = value
|
||||
def get_error_bg_color(self):
|
||||
return self._error_bg_color
|
||||
def reset_error_bg_color(self):
|
||||
self._error_bg_color = QColor(255, 255, 255)
|
||||
|
||||
error_bg_color_option = Property(QColor, get_error_bg_color, set_error_bg_color, reset_error_bg_color)
|
||||
|
||||
def set_warning_fg_color(self, value):
|
||||
self._warning_fg_color = value
|
||||
def get_warning_fg_color(self):
|
||||
return self._warning_fg_color
|
||||
def reset_warning_fg_color(self):
|
||||
self._warning_fg_color = QColor(255, 255, 0)
|
||||
|
||||
warning_fg_color_option = Property(QColor, get_warning_fg_color, set_warning_fg_color, reset_warning_fg_color)
|
||||
|
||||
def set_warning_bg_color(self, value):
|
||||
self._warning_bg_color = value
|
||||
def get_warning_bg_color(self):
|
||||
return self._warning_bg_color
|
||||
def reset_warning_bg_color(self):
|
||||
self._warning_bg_color = QColor(255, 255, 255)
|
||||
|
||||
warning_bg_color_option = Property(QColor, get_warning_bg_color, set_warning_bg_color, reset_warning_bg_color)
|
||||
|
||||
def set_debug_fg_color(self, value):
|
||||
self._debug_fg_color = value
|
||||
def get_debug_fg_color(self):
|
||||
return self._debug_fg_color
|
||||
def reset_debug_fg_color(self):
|
||||
self._debug_fg_color = QColor(128, 128, 128)
|
||||
|
||||
debug_fg_color_option = Property(QColor, get_debug_fg_color, set_debug_fg_color, reset_debug_fg_color)
|
||||
|
||||
def set_debug_bg_color(self, value):
|
||||
self._debug_bg_color = value
|
||||
def get_debug_bg_color(self):
|
||||
return self._debug_bg_color
|
||||
def reset_debug_bg_color(self):
|
||||
self._debug_bg_color = QColor(255, 255, 255)
|
||||
|
||||
debug_bg_color_option = Property(QColor, get_debug_bg_color, set_debug_bg_color, reset_debug_bg_color)
|
||||
|
||||
def set_info_fg_color(self, value):
|
||||
self._info_fg_color = value
|
||||
def get_info_fg_color(self):
|
||||
return self._info_fg_color
|
||||
def reset_info_fg_color(self):
|
||||
self._info_fg_color = QColor(0, 0, 0)
|
||||
|
||||
info_fg_color_option = Property(QColor, get_info_fg_color, set_info_fg_color, reset_info_fg_color)
|
||||
|
||||
def set_info_bg_color(self, value):
|
||||
self._info_bg_color = value
|
||||
def get_info_bg_color(self):
|
||||
return self._info_bg_color
|
||||
def reset_info_bg_color(self):
|
||||
self._info_bg_color = QColor(255, 255, 255)
|
||||
|
||||
info_bg_color_option = Property(QColor, get_info_bg_color, set_info_bg_color, reset_info_bg_color)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = MachineLog()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
796
lib/python/qtvcp/widgets/macro_widget.py
Normal file
@@ -0,0 +1,796 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp Widgets
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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 os
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.entry_widget import TouchInterface
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
from qtvcp.widgets.touchEntry import TouchDoubleSpinBox,TouchSpinBox
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# INFO holds INI file details
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
INFO = Info()
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Set the log level for this module
|
||||
if not INFO.LINUXCNC_IS_RUNNING:
|
||||
LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
try:
|
||||
from qtpy import QtSvg
|
||||
try:
|
||||
from qtpy.QtSvgWidgets import QSvgWidget
|
||||
except (ImportError, AttributeError):
|
||||
QSvgWidget = QtSvg.QSvgWidget
|
||||
except:
|
||||
LOG.critical("Qtvcp error with macro_widget - is package python3-pyqt5.qtsvg installed?")
|
||||
|
||||
###############################################################
|
||||
# helper widget for SVG display on Button
|
||||
#
|
||||
# We can add a svg image from a specific layer to QPushButton
|
||||
###############################################################
|
||||
class CustomButton(QtWidgets.QPushButton):
|
||||
def __init__(self, parent=None, path=None, layer=0):
|
||||
super(CustomButton, self).__init__(parent)
|
||||
if path is None:
|
||||
tpath = os.path.expanduser(INFO.MACRO_PATH)
|
||||
path = os.path.join(tpath, 'LatheMacro.svg')
|
||||
self.r = QtSvg.QSvgRenderer(path)
|
||||
self.basename = 'layer'
|
||||
self.setLayerNumber(layer)
|
||||
|
||||
def setLayerNumber(self, num):
|
||||
temp = '%s%d' % (self.basename, int(num))
|
||||
if self.r.elementExists(temp):
|
||||
self.Num = int(num)
|
||||
self.layer = temp
|
||||
else:
|
||||
self.Num = 0
|
||||
self.layer = 'layer0'
|
||||
LOG.error("MacrosTab SVG Button-No Layer Found: {}".format(temp))
|
||||
|
||||
def paintEvent(self, event):
|
||||
super(CustomButton, self).paintEvent(event)
|
||||
qp = QtGui.QPainter()
|
||||
qp.begin(self)
|
||||
self.r.render(qp, self.layer)
|
||||
qp.end()
|
||||
|
||||
|
||||
####################################################
|
||||
# helper widget for SVG display
|
||||
#
|
||||
# class to display certain layers of an svg file
|
||||
# instantiate it with layer number or
|
||||
# set layer number after with setLayerNumbet(int)
|
||||
####################################################
|
||||
class CustomSVG(QSvgWidget):
|
||||
def __init__(self, parent=None, layer=0):
|
||||
super(CustomSVG, self).__init__(parent)
|
||||
self.basename = 'layer'
|
||||
self.layer = 'layer%d' % layer
|
||||
self.num = layer
|
||||
|
||||
def setLayerNumber(self, num):
|
||||
temp = '%s%d' % (self.basename, int(num))
|
||||
if self.renderer().elementExists(temp):
|
||||
self.Num = int(num)
|
||||
self.layer = temp
|
||||
else:
|
||||
LOG.error("MacrosTab SVG-No Layer Found: {}".format(temp))
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(200, 200)
|
||||
|
||||
def paintEvent(self, event):
|
||||
qp = QtGui.QPainter()
|
||||
qp.begin(self)
|
||||
s = self.renderer()
|
||||
s.render(qp, self.layer)
|
||||
qp.end()
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Macro tab widget
|
||||
#
|
||||
# macro tab widget parses the subroutine path for /lathe
|
||||
# It then opens the .ngc files there and searches for keynames
|
||||
# using these key names it puts together a tab widget with svg file pics
|
||||
# the svg file should be in the same folder
|
||||
###############################################################################
|
||||
|
||||
DEFAULT = 0
|
||||
WARNING = 1
|
||||
CRITICAL = 2
|
||||
|
||||
class MacroTab(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(MacroTab, self).__init__(parent)
|
||||
|
||||
self.hilightStyle = "QLineEdit{border: 2px solid red;}"
|
||||
|
||||
# id names for what dialog we want launched
|
||||
self.load_dialog_code = 'LOAD'
|
||||
self.save_dialog_code = 'SAVE'
|
||||
self._request_name = 'CALCULATOR'
|
||||
|
||||
self.stack = QtWidgets.QStackedWidget()
|
||||
|
||||
# add some buttons to run,close and menu
|
||||
hbox = QtWidgets.QHBoxLayout()
|
||||
self.runButton = QtWidgets.QPushButton("Run")
|
||||
self.runButton.pressed.connect(self.runChecked)
|
||||
self.saveButton = QtWidgets.QPushButton("Save")
|
||||
self.saveButton.pressed.connect(self.saveChecked)
|
||||
self.saveButton.setVisible(False)
|
||||
|
||||
self.loadButton = QtWidgets.QPushButton("Load")
|
||||
self.loadButton.pressed.connect(self.loadChecked)
|
||||
self.loadButton.setVisible(False)
|
||||
self.closeButton = QtWidgets.QPushButton("Close")
|
||||
self.closeButton.pressed.connect(self.closeChecked)
|
||||
self.closeButton.setVisible(False)
|
||||
|
||||
menuButton = QtWidgets.QPushButton("Menu")
|
||||
menuButton.pressed.connect(self.menuChecked)
|
||||
hbox.addWidget(self.runButton)
|
||||
hbox.addWidget(self.loadButton)
|
||||
hbox.addWidget(self.saveButton)
|
||||
hbox.addWidget(self.closeButton)
|
||||
hbox.insertSpacing(1, 20)
|
||||
hbox.addWidget(menuButton)
|
||||
hbox.addStretch(0)
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(self.stack)
|
||||
vbox.addStretch(1)
|
||||
vbox.addLayout(hbox)
|
||||
# add all that stuff above to me
|
||||
self.setLayout(vbox)
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(200, 200)
|
||||
|
||||
def _hal_init(self):
|
||||
# add The macros to the stacked wodget
|
||||
self.buildStack(INFO.SUB_PATH_LIST)
|
||||
|
||||
# get current style of the line input
|
||||
self._oldstyle = self.runButton.styleSheet()
|
||||
|
||||
self.runButton.setEnabled(False)
|
||||
STATUS.connect('state-off', lambda w: self.runButton.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.runButton.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.runButton.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.runButton.setEnabled(False))
|
||||
STATUS.connect('not-all-homed', lambda w, axis: self.runButton.setEnabled(False or INFO.NO_HOME_REQUIRED))
|
||||
STATUS.connect('all-homed', lambda w: self.runButton.setEnabled(True))
|
||||
STATUS.connect('general',self.returnFromDialog)
|
||||
|
||||
# Build a stack per macro found
|
||||
# it finds the icon info from the macro file
|
||||
# using the magic comments parsed before this
|
||||
# first find macros
|
||||
# then build a menu page
|
||||
# then build the stack
|
||||
# anything goes wrong display an error page
|
||||
def buildStack(self, pathlist):
|
||||
def checkIfFloat(val):
|
||||
return all([ [any([i.isnumeric(), i in ['.','e']]) for i in val], len(val.split('.')) == 2] )
|
||||
|
||||
macroFlag = False
|
||||
for path in pathlist:
|
||||
if 'macro' in path:
|
||||
path = os.path.expanduser(path)
|
||||
tabName = self._findMacros(path)
|
||||
LOG.debug("Macros Found: {}".format(tabName))
|
||||
if tabName is None:
|
||||
continue
|
||||
macroFlag = True
|
||||
self._buildMenuPage(tabName,path)
|
||||
# Add pages
|
||||
# tabname is a list of found macros
|
||||
# these macro names are also used as the base name
|
||||
# of a list of required inputs.
|
||||
# we add a label and lineedit/radiobutton for each string in each
|
||||
# of these arrays
|
||||
for i, tName in enumerate(tabName):
|
||||
# make a widget that is added to the stack
|
||||
# TouchInterface can pop an entry dialog on focus
|
||||
w = TouchInterface(self)
|
||||
w.setObjectName(tName)
|
||||
w.keyboard_enable = True
|
||||
# redirect Touchinterface call to our function
|
||||
w.callDialog = self.getNumbers
|
||||
|
||||
# main layout of this tab
|
||||
hbox = QtWidgets.QHBoxLayout(w)
|
||||
#hbox.addStretch(1)
|
||||
|
||||
# vertical layout for labels and entries
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
|
||||
widgetList = []
|
||||
|
||||
# add labels and edits
|
||||
# self[tName][0] is the list of name text and defaults pairs
|
||||
for n, name in enumerate(self[tName][0]):
|
||||
LOG.verbose('{} {}'.format(tName, name))
|
||||
#print('int------>',self[tName])
|
||||
# if no list of names then continue looking
|
||||
if name[0]=='':continue
|
||||
|
||||
# layout holds label and entry for one line
|
||||
hbox2 = QtWidgets.QHBoxLayout()
|
||||
# make a label
|
||||
l = QtWidgets.QLabel(name[0])
|
||||
|
||||
# make appropriate entries:
|
||||
# radio/check/push buttons?
|
||||
if 'false' in name[1].lower() or 'true' in name[1].lower():
|
||||
if 'check' in name[1].lower():
|
||||
self['%s%d' % (tName, n)] = QtWidgets.QCheckBox()
|
||||
if 'true' in name[1].lower():
|
||||
self['%s%d' % (tName, n)].setChecked(True)
|
||||
elif 'radio' in name[1].lower():
|
||||
self['%s%d' % (tName, n)] = QtWidgets.QRadioButton()
|
||||
if 'true' in name[1].lower():
|
||||
self['%s%d' % (tName, n)].setChecked(True)
|
||||
elif 'button' in name[1].lower():
|
||||
self['%s%d' % (tName, n)] = QtWidgets.QPushButton()
|
||||
self['%s%d' % (tName, n)].setCheckable(True)
|
||||
if 'true' in name[1].lower():
|
||||
self['%s%d' % (tName, n)].setChecked(True)
|
||||
# legacy default
|
||||
else:
|
||||
self['%s%d' % (tName, n)] = QtWidgets.QRadioButton()
|
||||
if 'true' in name[1].lower():
|
||||
self['%s%d' % (tName, n)].setChecked(True)
|
||||
|
||||
# line edits that will pop an entry dialog:
|
||||
else:
|
||||
if checkIfFloat(name[1]):
|
||||
self['%s%d' % (tName, n)] = TouchDoubleSpinBox()
|
||||
self['%s%d' % (tName, n)].callDialog = self.getNumbers
|
||||
self['%s%d' % (tName, n)].setValue(float(name[1]))
|
||||
wName = "{}".format(name[0])
|
||||
self['%s%d' % (tName, n)].setObjectName(wName)
|
||||
widgetList.append([wName,'%s%d' % (tName, n)])
|
||||
#print('float-->',wName)
|
||||
else:
|
||||
self['%s%d' % (tName, n)] = TouchSpinBox()
|
||||
self['%s%d' % (tName, n)].callDialog = self.getNumbers
|
||||
self['%s%d' % (tName, n)].setValue(int(name[1]))
|
||||
wName = "{}".format(name[0])
|
||||
self['%s%d' % (tName, n)].setObjectName(wName)
|
||||
widgetList.append([wName,'%s%d' % (tName, n)])
|
||||
#print('int-->',wName)
|
||||
|
||||
self['%s%d' % (tName, n)]._label = name[0]
|
||||
self['%s%d' % (tName, n)]._tabName = tName
|
||||
self['%s%d' % (tName, n)].keyboard_type = 'numeric'
|
||||
|
||||
hbox2.addWidget(l)
|
||||
hbox2.addWidget(self['%s%d' % (tName, n)])
|
||||
|
||||
# add label/entry layout to vertical layout fr this tab
|
||||
vbox.addLayout(hbox2)
|
||||
|
||||
#add the SVG/image pic layer
|
||||
img_info = self[tName][1]
|
||||
#print path+svg_info[0], svg_info[1]
|
||||
# SVG?
|
||||
if img_info[0].endswith('.svg'):
|
||||
svgpath = os.path.join(path, img_info[0])
|
||||
self['sw%d' % i] = CustomSVG(svgpath, int(img_info[1]))
|
||||
self['sw%d' % i].setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
# some other supported image file?
|
||||
else:
|
||||
# get image path
|
||||
try:
|
||||
#print(self[tName][1][1])
|
||||
imgpath = os.path.join(path, self[tName][1][1])
|
||||
except:
|
||||
imgpath = os.path.join(path, img_info[0])
|
||||
|
||||
# use a qlabel to display image
|
||||
self['sw%d' % i] = QtWidgets.QLabel()
|
||||
self['sw%d' % i].setPixmap(QtGui.QPixmap(imgpath))
|
||||
self['sw%d' % i].setScaledContents(True)
|
||||
self['sw%d' % i].setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
|
||||
# add image to the main layout for this tab
|
||||
hbox.addWidget(self['sw%d' % i])
|
||||
# add label/entry stack to the main layout for this tab
|
||||
hbox.addLayout(vbox)
|
||||
# add this tab to the stack
|
||||
self.stack.addWidget(w)
|
||||
|
||||
# add the widget list used by the dialog
|
||||
# to cucle through entries
|
||||
self[tName][3] = widgetList
|
||||
|
||||
# No macros found in any path
|
||||
# show a message
|
||||
if macroFlag == False:
|
||||
self._buildErrorTab()
|
||||
|
||||
# Menu page has icon buttons to select the macro
|
||||
# it finds the icon info from the macro file
|
||||
# using the magic comments parsed before this
|
||||
def _buildMenuPage(self, tabNames,path):
|
||||
col = row = 0
|
||||
w = QtWidgets.QWidget()
|
||||
hbox = QtWidgets.QHBoxLayout(w)
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
grid = QtWidgets.QGridLayout()
|
||||
grid.setSpacing(10)
|
||||
# we grid them in columns of (arbitrarily) 5
|
||||
# hopefully we don't have too many macros...
|
||||
for i, tName in enumerate(tabNames):
|
||||
svg_name = self[tName][1][0]
|
||||
if svg_name.endswith('.svg'):
|
||||
try:
|
||||
svg_num = self[tName][1][2]
|
||||
except:
|
||||
svg_num = self[tName][1][1]
|
||||
svgpath = os.path.join(path, svg_name)
|
||||
# label is the only way I have found to make the buttons
|
||||
# larger - the label is under the pic - if no errors
|
||||
btn = CustomButton('Oops\n', path=svgpath, layer=svg_num)
|
||||
else:
|
||||
imgpath = os.path.join(path, svg_name)
|
||||
btn = QtWidgets.QPushButton()
|
||||
btn.setStyleSheet("border-image: url(" + imgpath + ");")
|
||||
btn.setToolTip('Macro: {}'.format(tName))
|
||||
btn.setWhatsThis('This button will select The entry page for the {} macro'.format(tName))
|
||||
btn.clicked.connect(self.menuButtonPress(i))
|
||||
btn.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
|
||||
QtWidgets.QSizePolicy.Preferred)
|
||||
grid.addWidget(btn, row, col, 1, 1)
|
||||
row += 1
|
||||
if row > 4:
|
||||
row = 0
|
||||
col += 1
|
||||
vbox.addLayout(grid)
|
||||
vbox.addStretch(1)
|
||||
hbox.addLayout(vbox)
|
||||
hbox.addStretch(1)
|
||||
# add the widget to our stack
|
||||
self.stack.addWidget(w)
|
||||
|
||||
# make something so the user may have some small clue.
|
||||
# probably should do more - subroutines/macros are not user friendly
|
||||
def _buildErrorTab(self):
|
||||
w = QtWidgets.QWidget()
|
||||
vbox = QtWidgets.QVBoxLayout(w)
|
||||
vbox.addStretch(1)
|
||||
mess = QtWidgets.QLabel('No Usable Macros Found.\nLooked in path(s) specified in INI file under heading:\n[RS274NGC],\nSUBROUTINE_PATH=')
|
||||
vbox.addWidget(mess)
|
||||
mess = QtWidgets.QLabel( '\n'.join(map(str, INFO.SUB_PATH_LIST)))
|
||||
vbox.addWidget(mess)
|
||||
# add the widget to the stack
|
||||
self.stack.addWidget(w)
|
||||
|
||||
# search for special macros that have the magic comments
|
||||
# compiles them into a complicated list for each macro
|
||||
# self['macroname'] = [ [DEFAULT DATA],[SVG FILE,LAYER,ICON LAYER],{OPTION DICT NAME:OPTION DICT DATA,}]
|
||||
# returns a list on the macro names that it finds valid
|
||||
|
||||
def _findMacros(self, path):
|
||||
tName = []
|
||||
macros = []
|
||||
defaults = []
|
||||
svg_data = []
|
||||
try:
|
||||
# look for NGC macros in path
|
||||
for f in os.listdir(path):
|
||||
if f.endswith('.ngc'):
|
||||
# open and look at the first three lines
|
||||
# these will gives us the info we need
|
||||
# to create the macros page
|
||||
with open(os.path.join(path, f), 'r') as temp:
|
||||
first_line = temp.readline().strip()
|
||||
second_line = temp.readline().strip()
|
||||
third_line = temp.readline().strip()
|
||||
fourth_line = temp.readline().strip()
|
||||
# check if they have the magic comments
|
||||
if 'MACROCOMMAND' in first_line and \
|
||||
'MACRODEFAULT' in second_line and \
|
||||
('MACROSVG' in third_line or
|
||||
'MACROIMAGE' in third_line):
|
||||
name = os.path.splitext(f)[0]
|
||||
# yes, now keep everything after '='
|
||||
macros = first_line.split('=')[1]
|
||||
defaults = second_line.split('=')[1]
|
||||
svg_data = third_line.split('=')[1]
|
||||
# we use a comma to break up titles
|
||||
m = macros.split(',')
|
||||
d = defaults.split(',')
|
||||
s = svg_data.split(',')
|
||||
# combine titles with defaults in to a list
|
||||
for num, (m_item, d_item) in enumerate(zip(m, d)):
|
||||
#print num,m_item,d_item
|
||||
if num == 0:
|
||||
temp = []
|
||||
temp.append((m_item, d_item))
|
||||
continue
|
||||
temp.append((m_item, d_item))
|
||||
|
||||
# look for options like save/load buttons
|
||||
# it should be the fourth line in sub program
|
||||
# it's put in a dict to future proof it
|
||||
# and make it easier to parse
|
||||
option_dict={}
|
||||
if 'MACROOPTIONS' in fourth_line:
|
||||
options = fourth_line.split('=')[1]
|
||||
options = options.lstrip(' ')
|
||||
o = options.split(',')
|
||||
for i in(o):
|
||||
h,g = i.split(':')
|
||||
option_dict['%s'%h.upper()]=g
|
||||
#print (option_dict)
|
||||
|
||||
# add the list then add svg info, then options
|
||||
self[name] = [temp]
|
||||
self[name].append(s)
|
||||
self[name].append(option_dict)
|
||||
self[name].append([])
|
||||
#print'group:',name, self[name]
|
||||
# make a list of pages, which is also the macro program name
|
||||
tName.append(name)
|
||||
except (TypeError):
|
||||
return None
|
||||
except OSError as e:
|
||||
LOG.debug('Path: {} is not valid.'.format(path))
|
||||
return None
|
||||
except Exception as e:
|
||||
LOG.debug('Exception loading Macros:', exc_info=e)
|
||||
return None
|
||||
if tName == []:
|
||||
return None
|
||||
return tName
|
||||
|
||||
# This figures out what macro is showing
|
||||
# and builds the macro command from the data
|
||||
# then sends it to the controller
|
||||
def runMacro(self):
|
||||
cmd = ''
|
||||
name = str(self.stack.currentWidget().objectName())
|
||||
if name == '': return
|
||||
macro = name
|
||||
#print 'macro', macro
|
||||
for num, i in enumerate(self[name][0]):
|
||||
# check for macro that needs no data
|
||||
if i == ('', ''):break
|
||||
# Look for button widget instance so we can convert to integers
|
||||
# other wise we assume text
|
||||
if isinstance(self['%s%d' % (name, num)], QtWidgets.QRadioButton) or \
|
||||
isinstance(self['%s%d' % (name, num)], QtWidgets.QCheckBox) or \
|
||||
isinstance(self['%s%d' % (name, num)], QtWidgets.QPushButton):
|
||||
data = str(1 * int(self['%s%d' % (name, num)].isChecked()))
|
||||
else:
|
||||
data = str(self['%s%d' % (name, num)].text())
|
||||
if data != '':
|
||||
cmd = cmd + '[' + data + '] '
|
||||
command = str("O<" + macro + "> call " + cmd)
|
||||
LOG.debug("Macro command: {}".format(command))
|
||||
ACTION.CALL_MDI(command)
|
||||
|
||||
# This could be 'class patched' to do something else
|
||||
# the macro dialog does this
|
||||
def runChecked(self):
|
||||
self.runMacro()
|
||||
|
||||
# This could be 'class patched' to do something else
|
||||
def saveChecked(self):
|
||||
self.savePressed()
|
||||
|
||||
# This could be 'class patched' to do something else
|
||||
def loadChecked(self):
|
||||
self.loadPressed()
|
||||
|
||||
# This could be 'class patched' to do something else
|
||||
def closeChecked(self):
|
||||
self.stack.setCurrentIndex(0)
|
||||
|
||||
# brings the menu selection page to the front
|
||||
def menuChecked(self):
|
||||
self.stack.setCurrentIndex(0)
|
||||
self.setTitle('Qtvcp Macro Menu')
|
||||
self.loadButton.setVisible(False)
|
||||
self.saveButton.setVisible(False)
|
||||
|
||||
# This weird code is just so we can get the index
|
||||
# number from a menu button press
|
||||
# using clicked.connect() apparently doesn't easily
|
||||
# add user data
|
||||
def menuButtonPress(self, data):
|
||||
def calluser():
|
||||
self.stack.setCurrentIndex(data+1)
|
||||
try:
|
||||
name = self.stack.currentWidget().objectName()
|
||||
self.setTitle(name)
|
||||
self.set_statusbar('Macro Tab:{}'.format(name.upper()))
|
||||
# show these buttons if the macro specifies it
|
||||
for name in (self[name][2]):
|
||||
if name == 'LOAD':
|
||||
self.loadButton.setVisible(True)
|
||||
if name == 'SAVE':
|
||||
self.saveButton.setVisible(True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return calluser
|
||||
|
||||
def loadPressed(self):
|
||||
self.getFileName()
|
||||
|
||||
def savePressed(self):
|
||||
self.getSaveFileName()
|
||||
|
||||
def openReturn(self, path):
|
||||
LOG.debug("Open return filename chosen: {}".format(path))
|
||||
file = QtCore.QFile(path)
|
||||
file.open(QtCore.QFile.ReadOnly)
|
||||
name = str(self.stack.currentWidget().objectName())
|
||||
while not file.atEnd():
|
||||
readLine = file.readLine()
|
||||
try:
|
||||
# Python v2.
|
||||
readLine = str(readLine, encoding='utf8')
|
||||
except NameError:
|
||||
# Python v3.
|
||||
readLine = str(readLine, encoding='utf8')
|
||||
widget,data,title = readLine.split(',')
|
||||
#print (widget,data,bool(data),title,name)
|
||||
if name in widget:
|
||||
# set widgets to data:
|
||||
# Look for a radio button instance so we can convert to integers
|
||||
# other wise we assume text
|
||||
if isinstance(self[widget], QtWidgets.QRadioButton) or \
|
||||
isinstance(self[widget], QtWidgets.QCheckBox) or \
|
||||
isinstance(self[widget], QtWidgets.QPushButton):
|
||||
self[widget].setChecked(int(data))
|
||||
else:
|
||||
self[widget].setText(str(data))
|
||||
|
||||
# save the current screen data to file picked by the user.
|
||||
# it's a plain text file
|
||||
def saveReturn(self, path):
|
||||
LOG.debug("Save return filename chosen: {}".format(path))
|
||||
name = str(self.stack.currentWidget().objectName())
|
||||
if name == '': return
|
||||
file = QtCore.QFile(path)
|
||||
if file.open(QtCore.QFile.WriteOnly):
|
||||
for num, i in enumerate(self[name][0]):
|
||||
widgetname = '%s%d' % (name, num)
|
||||
# Look for a radio button instance so we can convert to bool
|
||||
# other wise we assume text
|
||||
if isinstance(self[widgetname], QtWidgets.QRadioButton) or \
|
||||
isinstance(self[widgetname], QtWidgets.QCheckBox) or \
|
||||
isinstance(self[widgetname], QtWidgets.QPushButton):
|
||||
data = str(1 * int(self[widgetname].isChecked()))
|
||||
else:
|
||||
data = str(self[widgetname].text())
|
||||
line = '%s,%s, %s\n'%( widgetname, str(data), i[0])
|
||||
QtCore.QTextStream(file) << line
|
||||
else:
|
||||
QMessageBox.information(self, "Unable to open file",
|
||||
file.errorString())
|
||||
|
||||
# we do this instead of directly so the dialog version's title changes
|
||||
# when it's overridden
|
||||
def setTitle(self, string):
|
||||
self.setWindowTitle(string)
|
||||
|
||||
# get numeric data
|
||||
def getNumbers(self, widget, ktype=None, next=False):
|
||||
# remove overlay so style can be changed
|
||||
STATUS.emit('focus-overlay-changed', False, None, None)
|
||||
# record the original style
|
||||
self._oldstyle = widget.styleSheet()
|
||||
# change to highlight style
|
||||
widget.setStyleSheet(self.hilightStyle)
|
||||
QApplication.processEvents()
|
||||
|
||||
# dialog call
|
||||
mess = {'NAME':self._request_name,
|
||||
'ID':'%s__macro' % self.objectName(),
|
||||
'PRELOAD':float(widget.text()),
|
||||
'TITLE':'{} Macro Entry For {}'.format(widget._tabName,widget._label),
|
||||
'WIDGET':widget,
|
||||
'NEXT':next,
|
||||
'WIDGETCYCLE': True
|
||||
}
|
||||
LOG.debug("get numbers {}".format(mess))
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
# request the system to pop a load path picker dialog
|
||||
# do this so the system is consistent and things like dialog
|
||||
# placement are done.
|
||||
def getFileName(self):
|
||||
macro = self.stack.currentWidget().objectName()
|
||||
dirct = self[macro][2].get('PATH')
|
||||
defaultfilename = self[macro][2].get('DEFAULT')
|
||||
|
||||
mess = {'NAME':self.load_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'Load Macro',
|
||||
'EXTENSIONS':'Text Files (*.txt);;ALL Files (*.*)'
|
||||
}
|
||||
if not defaultfilename is None:
|
||||
mess['FILENAME'] = defaultfilename
|
||||
else:
|
||||
mess['FILENAME'] = '{}_data.txt'.format( str(self.stack.currentWidget().objectName()))
|
||||
if not dirct is None:
|
||||
mess['DIRECTORY'] = dirct
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
# request the system to pop a save path picker dialog
|
||||
# do this so the system is consistent and things like dialog
|
||||
# placement are done.
|
||||
def getSaveFileName(self):
|
||||
macro = self.stack.currentWidget().objectName()
|
||||
dirct = self[macro][2].get('PATH')
|
||||
defaultfilename = self[macro][2].get('DEFAULT')
|
||||
|
||||
mess = {'NAME':self.save_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'Save Macro',
|
||||
'EXTENSIONS':'Text Files (*.txt);;ALL Files (*.*)'
|
||||
}
|
||||
if not defaultfilename is None:
|
||||
mess['FILENAME'] = defaultfilename
|
||||
else:
|
||||
mess['FILENAME'] = '{}_data.txt'.format( str(self.stack.currentWidget().objectName()))
|
||||
if not dirct is None:
|
||||
mess['DIRECTORY'] = dirct
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
# process the STATUS return message from dialogs
|
||||
def returnFromDialog(self, w, message):
|
||||
if message.get('NAME') == self.load_dialog_code:
|
||||
path = message.get('RETURN')
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
if path and code:
|
||||
self.openReturn(path)
|
||||
elif message.get('NAME') == self.save_dialog_code:
|
||||
path = message.get('RETURN')
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
if path and code:
|
||||
self.saveReturn(path)
|
||||
elif message.get('NAME') == self._request_name:
|
||||
num = message.get('RETURN')
|
||||
code = bool(message.get('ID') == '%s__macro'% self.objectName())
|
||||
widget = message.get('WIDGET')
|
||||
next = message.get('NEXT', False)
|
||||
back = message.get('BACK', False)
|
||||
if code and widget is not None:
|
||||
widget.setStyleSheet(self._oldstyle)
|
||||
if num is not None:
|
||||
widget.setValue(num)
|
||||
# clear high lighting
|
||||
|
||||
# request for next input widget from parm_list
|
||||
if next or back:
|
||||
name = self.stack.currentWidget().objectName()
|
||||
# find current widget in list
|
||||
for num,data in enumerate(self[name][3]):
|
||||
txt,widname = data
|
||||
if txt == widget.objectName():
|
||||
if next:
|
||||
num +=1
|
||||
if num == len(self[name][3]):
|
||||
num = 0
|
||||
else:
|
||||
num -=1
|
||||
if num == -1:
|
||||
num = len(self[name][3])-1
|
||||
# build reference to the next widget
|
||||
newobj = self[self[name][3][num][1]]
|
||||
break
|
||||
|
||||
# update the dialog
|
||||
self.getNumbers(newobj,None, next=True)
|
||||
|
||||
#####################################################
|
||||
# Helper functions
|
||||
#####################################################
|
||||
|
||||
# return false if failed so other ways of reporting can be used.
|
||||
# there might not be a statusbar in main screen.
|
||||
def set_statusbar(self, msg, priority = DEFAULT, noLog = False):
|
||||
try:
|
||||
self.QTVCP_INSTANCE_.add_status(msg, priority, noLog)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_style(self, widget):
|
||||
widget.setStyleSheet(
|
||||
"""
|
||||
QDoubleSpinBox,
|
||||
QSpinBox {
|
||||
padding-right: 15px; /* make room for the arrows */
|
||||
border-image: url(:/images/frame.png) 4;
|
||||
border-width: 3;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
font-size:20px;
|
||||
border: 1px solid black;
|
||||
min-width: 100px;
|
||||
max-height: 30;
|
||||
}
|
||||
|
||||
QDoubleSpinBox::up-button,
|
||||
QSpinBox::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: right; width: 30px; height: 28px;
|
||||
image: url(:/widget/images/arrow_up1.png);
|
||||
border: 1px solid black;
|
||||
}
|
||||
QDoubleSpinBox::up-button:pressed,
|
||||
QSpinBox::up-button:pressed {
|
||||
image: url(:/widget/images/arrow_up2.png);
|
||||
}
|
||||
|
||||
QDoubleSpinBox::down-button,
|
||||
QSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: left; width: 30px; height: 28px;
|
||||
image: url(:/widget/images/arrow_down1.png);
|
||||
border: 1px solid black;
|
||||
}
|
||||
QDoubleSpinBox::down-button:pressed,
|
||||
QSpinBox::down-button:pressed {
|
||||
image: url(:/widget/images/arrow_down2.png) 1;
|
||||
}
|
||||
|
||||
""")
|
||||
|
||||
# usual boiler code
|
||||
# (used so we can use code such as self[SomeDataName]
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# STATUS may cause seg fault testing here
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
#sw = QtSvg.QSvgWidget('LatheMacro.svg')
|
||||
sw = MacroTab()
|
||||
sw.buildStack(['~/linuxcnc/nc_files/examples/macros/lathe'])
|
||||
sw.setGeometry(50, 50, 759, 668)
|
||||
sw.show()
|
||||
sys.exit(app.exec())
|
||||
189
lib/python/qtvcp/widgets/mdi_history.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget - MDI history widget
|
||||
# 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 os
|
||||
|
||||
from qtpy.QtWidgets import QWidget, QVBoxLayout, QListView
|
||||
from qtpy.QtCore import Property, QSize, QModelIndex, QItemSelectionModel, QItemSelection, QPoint
|
||||
from qtpy.QtGui import QStandardItemModel, QStandardItem
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.mdi_line import MDILine
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
class MDIHistory(QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(MDIHistory, self).__init__(parent)
|
||||
self.setMinimumSize(QSize(200, 150))
|
||||
self.setWindowTitle("PyQt5 editor test example")
|
||||
|
||||
lay = QVBoxLayout()
|
||||
lay.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(lay)
|
||||
|
||||
self.list = QListView()
|
||||
self.list.setEditTriggers(QListView.NoEditTriggers)
|
||||
self.list.activated.connect(self.activated)
|
||||
self.list.setAlternatingRowColors(True)
|
||||
self.list.selectionChanged = self.selectionChanged
|
||||
self.model = QStandardItemModel(self.list)
|
||||
|
||||
self.MDILine = MDILine()
|
||||
self.MDILine.soft_keyboard = False
|
||||
self.MDILine.line_up = self.line_up
|
||||
self.MDILine.line_down = self.line_down
|
||||
|
||||
STATUS.connect('mdi-history-changed', self.reload)
|
||||
|
||||
# add widgets
|
||||
lay.addWidget(self.list)
|
||||
lay.addWidget(self.MDILine)
|
||||
|
||||
self.fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
try:
|
||||
open(self.fp, 'r')
|
||||
LOG.debug('MDI History file found: {}'.format(self.fp))
|
||||
except:
|
||||
open(self.fp, 'x')
|
||||
LOG.debug('MDI History file created: {}'.format(self.fp))
|
||||
self.reload()
|
||||
|
||||
def _hal_init(self):
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(not STATUS.is_auto_mode()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(STATUS.machine_is_on()))
|
||||
|
||||
def reload(self, w=None ):
|
||||
self.model.clear()
|
||||
try:
|
||||
self.rows = 0
|
||||
with open(self.fp,'r') as inputfile:
|
||||
for line in inputfile:
|
||||
line = line.rstrip('\n')
|
||||
item = QStandardItem(line)
|
||||
self.model.appendRow(item)
|
||||
self.rows += 1
|
||||
if isinstance(self.sender(), QListView):
|
||||
self.select_row('down')
|
||||
else:
|
||||
self.row = self.rows
|
||||
self.list.setModel(self.model)
|
||||
if self.row == self.rows:
|
||||
self.list.scrollToBottom()
|
||||
self.MDILine.setText('')
|
||||
except:
|
||||
LOG.debug('File path is not valid: {}'.format(self.fp))
|
||||
|
||||
def selectionChanged(self,old, new):
|
||||
cmd = self.getSelected()
|
||||
self.MDILine.setText(cmd)
|
||||
selectionModel = self.list.selectionModel()
|
||||
self.row = selectionModel.currentIndex().row()
|
||||
|
||||
def getSelected(self):
|
||||
selected_indexes = self.list.selectedIndexes()
|
||||
selected_rows = [item.row() for item in selected_indexes]
|
||||
# iterates each selected row in descending order
|
||||
for selected_row in sorted(selected_rows, reverse=True):
|
||||
text = self.model.item(selected_row).text()
|
||||
return text
|
||||
|
||||
def activated(self):
|
||||
cmd = self.getSelected()
|
||||
self.MDILine.setText(cmd)
|
||||
self.MDILine.submit()
|
||||
|
||||
def run_command(self):
|
||||
self.MDILine.submit()
|
||||
self.select_row('last')
|
||||
|
||||
def select_row(self, style):
|
||||
style = style.lower()
|
||||
selectionModel = self.list.selectionModel()
|
||||
parent = QModelIndex()
|
||||
self.rows = self.model.rowCount(parent) - 1
|
||||
if style == 'last':
|
||||
self.row = self.rows
|
||||
elif style == 'first':
|
||||
self.row = 0
|
||||
elif style == 'up':
|
||||
if self.row > 0:
|
||||
self.row -= 1
|
||||
else:
|
||||
self.row = 0
|
||||
elif style == 'down':
|
||||
if self.row < self.rows:
|
||||
self.row += 1
|
||||
else:
|
||||
self.row = self.rows
|
||||
else:
|
||||
return
|
||||
top = self.model.index(self.row, 0, parent)
|
||||
bottom = self.model.index(self.row, 0, parent)
|
||||
selectionModel.setCurrentIndex(top, QItemSelectionModel.Select | QItemSelectionModel.Rows)
|
||||
selection = QItemSelection(top, top)
|
||||
selectionModel.clearSelection()
|
||||
selectionModel.select(selection, QItemSelectionModel.Select)
|
||||
|
||||
def line_up(self):
|
||||
self.select_row('up')
|
||||
|
||||
def line_down(self):
|
||||
self.select_row('down')
|
||||
|
||||
#########################################################################
|
||||
# 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 set_soft_keyboard(self, data):
|
||||
self.MDILine.soft_keyboard = data
|
||||
def get_soft_keyboard(self):
|
||||
return self.MDILine.soft_keyboard
|
||||
def reset_soft_keyboard(self):
|
||||
self.MDILine.soft_keyboard = False
|
||||
|
||||
# designer will show these properties in this order:
|
||||
soft_keyboard_option = Property(bool, get_soft_keyboard, set_soft_keyboard, reset_soft_keyboard)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = MDIHistory()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
355
lib/python/qtvcp/widgets/mdi_line.py
Normal file
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget - MDI edit line widget
|
||||
# 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 os
|
||||
import linuxcnc
|
||||
import hal
|
||||
import time
|
||||
import re
|
||||
|
||||
import subprocess
|
||||
|
||||
from qtpy.QtWidgets import QLineEdit, QApplication
|
||||
from qtpy.QtCore import Qt, QEvent, Property
|
||||
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp.widgets.entry_widget import SoftInputWidget
|
||||
from qtvcp.lib.aux_program_loader import Aux_program_loader
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
AUX_PRGM = Aux_program_loader()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
class MDI(QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
super(MDI, self).__init__(parent)
|
||||
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(not STATUS.is_auto_mode()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(STATUS.machine_is_on()))
|
||||
STATUS.connect('mdi-line-selected', self.external_line_selected)
|
||||
STATUS.connect('general',self.return_value)
|
||||
STATUS.connect('error', self.error_update)
|
||||
self.returnPressed.connect(self.submit)
|
||||
self.spindleInhibit = False
|
||||
self.g92Inhibit = False
|
||||
try:
|
||||
fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
f = open(fp, 'r')
|
||||
self.mdiLast = f.readlines()[-1].lower().strip() or None
|
||||
f.close()
|
||||
LOG.debug('MDI History file found: {}'.format(fp))
|
||||
except:
|
||||
try:
|
||||
fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
f = open(fp, 'x')
|
||||
f.close()
|
||||
LOG.debug('MDI History file created: {}'.format(fp))
|
||||
except:
|
||||
pass
|
||||
self.mdiLast = None
|
||||
|
||||
def getMDIText(self):
|
||||
text = str(self.text()).strip()
|
||||
return text
|
||||
|
||||
def submit(self):
|
||||
self.mdiError = False
|
||||
text = str(self.text()).strip()
|
||||
if text == '': return
|
||||
if text == 'HALMETER':
|
||||
AUX_PRGM.load_halmeter()
|
||||
elif text.startswith('HALMETER'):
|
||||
args = text.replace('HALMETER', '').strip()
|
||||
AUX_PRGM.load_halmeter(args)
|
||||
elif text == 'HALSHOW':
|
||||
AUX_PRGM.load_halshow()
|
||||
elif text.startswith('HALSHOW'):
|
||||
args = text.replace('HALSHOW', '').strip()
|
||||
AUX_PRGM.load_halshow(args)
|
||||
elif text == 'HALSCOPE':
|
||||
AUX_PRGM.load_halscope()
|
||||
elif text.startswith('HALSCOPE'):
|
||||
args = text.replace('HALSCOPE', '').strip()
|
||||
AUX_PRGM.load_halscope(args)
|
||||
elif text == 'STATUS':
|
||||
AUX_PRGM.load_status()
|
||||
elif text == 'CLASSICLADDER':
|
||||
AUX_PRGM.load_ladder()
|
||||
elif text == 'HALSCOPE':
|
||||
AUX_PRGM.load_halscope()
|
||||
elif text == 'CALIBRATION':
|
||||
AUX_PRGM.load_calibration()
|
||||
elif text == 'TESTLED':
|
||||
AUX_PRGM.load_test_led()
|
||||
elif text == 'TESTBUTTON':
|
||||
AUX_PRGM.load_test_button()
|
||||
elif text == 'TESTDIAL':
|
||||
AUX_PRGM.load_test_dial()
|
||||
elif text == 'PREFERENCE':
|
||||
STATUS.emit('show-preference')
|
||||
elif text == 'CLEAR HISTORY':
|
||||
fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
fp = open(fp, 'w')
|
||||
fp.close()
|
||||
elif text.lower().startswith('setp'):
|
||||
self.setp(text)
|
||||
elif text.lower().startswith('unlinkp'):
|
||||
self.unlinkp(text)
|
||||
elif text.lower().startswith('net'):
|
||||
self.net(text)
|
||||
elif self.spindleInhibit and self.inhibit_spindle_commands(text):
|
||||
return
|
||||
elif self.g92Inhibit and self.inhibit_g92(text):
|
||||
return
|
||||
else:
|
||||
ACTION.CALL_MDI(text+'\n')
|
||||
|
||||
# var file update with display reload is necessary for g10 commands to redraw the preview at the new rotation.
|
||||
# without this, the WCS and grid rotate, but the preview remains at the previous rotation.
|
||||
if 'g10' in text.lower():
|
||||
linuxcnc.command().task_plan_synch()
|
||||
ACTION.RELOAD_DISPLAY()
|
||||
t = time.time() + 0.1
|
||||
while time.time() < t:
|
||||
QApplication.processEvents()
|
||||
if not self.mdiError and text.lower() != self.mdiLast and text != 'CLEAR HISTORY':
|
||||
try:
|
||||
fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
fp = open(fp, 'a')
|
||||
fp.write(text + "\n")
|
||||
fp.close()
|
||||
except:
|
||||
pass
|
||||
if not self.mdiError:
|
||||
self.mdiLast = text.lower()
|
||||
STATUS.emit('mdi-history-changed')
|
||||
|
||||
# Gcode widget can emit a signal to this
|
||||
def external_line_selected(self, w, text, filename):
|
||||
LOG.debug('Ext line selected: {}, {}'.format(text, filename))
|
||||
if filename == INFO.MDI_HISTORY_PATH:
|
||||
self.setText(text)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
super(MDI, self).keyPressEvent(event)
|
||||
if event.key() == Qt.Key_Up:
|
||||
self.line_up()
|
||||
if event.key() == Qt.Key_Down:
|
||||
self.line_down()
|
||||
|
||||
def line_up(self):
|
||||
LOG.debug('up')
|
||||
STATUS.emit('move-text-lineup')
|
||||
|
||||
def line_down(self):
|
||||
LOG.debug('down')
|
||||
STATUS.emit('move-text-linedown')
|
||||
|
||||
def setp(self, setpString):
|
||||
arguments = len(setpString.lower().replace('setp',' ').split())
|
||||
if arguments == 2:
|
||||
halpin, value = setpString.lower().replace('setp',' ').split()
|
||||
else:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nsetp requires 2 arguments, {} given\n'.format(arguments))
|
||||
return
|
||||
try:
|
||||
hal.get_value(halpin)
|
||||
except Exception as err:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\n{}\n'.format(err))
|
||||
return
|
||||
try:
|
||||
hal.set_p(halpin, value)
|
||||
except Exception as err:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\n"{}" {}\n'.format(halpin, err))
|
||||
return
|
||||
if type(hal.get_value(halpin)) == bool:
|
||||
if value.lower() in ['true', '1']:
|
||||
value = True
|
||||
elif value.lower() in ['false', '0']:
|
||||
value = False
|
||||
else:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nValue "{}" invalid for a BIT pin/parameter\n'.format(value))
|
||||
return
|
||||
if hal.get_value(halpin) != value:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nBIT value comparison error\n')
|
||||
return
|
||||
elif type(hal.get_value(halpin)) == float:
|
||||
try:
|
||||
value = float(value)
|
||||
except:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nValue "{}" invalid for a Float pin/parameter\n'.format(value))
|
||||
return
|
||||
if hal.get_value(halpin) != value:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nFloat value comparison error\n')
|
||||
return
|
||||
else:
|
||||
try:
|
||||
value = int(value)
|
||||
except:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nValue "{}" invalid for S32 or U32 pin/parameter\n'.format(value))
|
||||
return
|
||||
if hal.get_value(halpin) != value:
|
||||
ACTION.SET_ERROR_MESSAGE('SETP ERROR:\nS32 or U32 value comparison error\n')
|
||||
return
|
||||
|
||||
def unlinkp(self, unlinkpString):
|
||||
arguments = len(unlinkpString.lower().replace('unlinkp',' ').split())
|
||||
if arguments == 1:
|
||||
halpin = unlinkpString.lower().replace('unlinkp',' ').strip()
|
||||
else:
|
||||
ACTION.SET_ERROR_MESSAGE('UNLINKP ERROR:\nunlinkp requires 1 argument, {} given\n'.format(arguments))
|
||||
return
|
||||
reply = hal.disconnect(halpin)
|
||||
if reply:
|
||||
ACTION.SET_ERROR_MESSAGE('UNLINKP ERROR:\nPin "{}" not found\n'.format(halpin))
|
||||
|
||||
def net(self, netString):
|
||||
arguments = len(netString.lower().replace('net',' ').split())
|
||||
if arguments >= 2:
|
||||
args = ['halcmd', 'net']
|
||||
split = netString.lower().replace('net',' ').split()
|
||||
for arg in split:
|
||||
args.append(arg)
|
||||
else:
|
||||
ACTION.SET_ERROR_MESSAGE('NET ERROR:\nnet requires at least 2 arguments, {} given\n'.format(arguments))
|
||||
return
|
||||
reply = subprocess.Popen(args, stderr=subprocess.PIPE)
|
||||
stdout, stderr = reply.communicate()
|
||||
if stderr:
|
||||
error = stderr.decode().replace('<commandline>:0:', '').strip()
|
||||
ACTION.SET_ERROR_MESSAGE('NET ERROR:\n{}\n'.format(error))
|
||||
|
||||
def spindle_inhibit(self, state):
|
||||
self.spindleInhibit = state
|
||||
|
||||
def g92_inhibit(self, state):
|
||||
self.g92Inhibit = state
|
||||
|
||||
# inhibit M3, M4, and M5 commands for plasma configs using the plasmac component, allow codes like M52P1
|
||||
def inhibit_spindle_commands(self, text):
|
||||
# Match spindle control only (M3, M4, M5), allow codes like M5x
|
||||
match = re.findall(r'M[345](?!\d)', text.upper().replace(" ", ""))
|
||||
if match:
|
||||
mCodes = ', '.join(match)
|
||||
ACTION.SET_ERROR_MESSAGE(f'MDI ERROR:\n{mCodes} commands are not allowed in MDI mode\n')
|
||||
return(1)
|
||||
return(0)
|
||||
|
||||
# inhibit g92 offsets for plasma configs using the plasmac component
|
||||
def inhibit_g92(self, text):
|
||||
if 'g92' in text.lower().replace(' ','') and not 'g92.1' in text.lower().replace(' ',''):
|
||||
ACTION.SET_ERROR_MESSAGE('MDI ERROR:\nG92 offsets are not allowed\n')
|
||||
return(1)
|
||||
return(0)
|
||||
|
||||
def error_update(self, obj, kind, error):
|
||||
if kind == linuxcnc.OPERATOR_ERROR:
|
||||
self.mdiError = True
|
||||
|
||||
class MDILine(MDI):
|
||||
def __init__(self, parent=None):
|
||||
super(MDILine, self).__init__(parent)
|
||||
self._PARENT_WIDGET = parent
|
||||
self.dialog_code = 'KEYBOARD'
|
||||
self.soft_keyboard = False
|
||||
self.dialog_keyboard = False
|
||||
self._input_panel_full = SoftInputWidget(self, 'default')
|
||||
self.installEventFilter(self)
|
||||
|
||||
# try/except is so designer will load
|
||||
def eventFilter(self, widget, event):
|
||||
if self.focusWidget() == widget and event.type() == QEvent.MouseButtonPress:
|
||||
if self.soft_keyboard:
|
||||
self._input_panel_full.show_input_panel(widget)
|
||||
elif self.dialog_keyboard:
|
||||
self.request_keyboard()
|
||||
try:
|
||||
ACTION.SET_MDI_MODE()
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def request_keyboard(self):
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'MDI',
|
||||
'GEONAME':'MDI_Keyboard_Dialog_{}'.format(self.dialog_code),
|
||||
}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
text = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and text is not None:
|
||||
self.setFocus()
|
||||
self.setText(text)
|
||||
self.submit()
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
STATUS.emit('update-machine-log', 'Set MDI {}'.format(text), 'TIME,SUCCESS')
|
||||
|
||||
#########################################################################
|
||||
# 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 set_soft_keyboard(self, data):
|
||||
self.soft_keyboard = data
|
||||
def get_soft_keyboard(self):
|
||||
return self.soft_keyboard
|
||||
def reset_soft_keyboard(self):
|
||||
self.soft_keyboard = False
|
||||
|
||||
# designer will show these properties in this order:
|
||||
soft_keyboard_option = Property(bool, get_soft_keyboard, set_soft_keyboard, reset_soft_keyboard)
|
||||
|
||||
def set_dialog_keyboard(self, data):
|
||||
self.dialog_keyboard = data
|
||||
def get_dialog_keyboard(self):
|
||||
return self.dialog_keyboard
|
||||
def reset_dialog_keyboard(self):
|
||||
self.dialog_keyboard = False
|
||||
|
||||
# designer will show these properties in this order:
|
||||
dialog_keyboard_option = Property(bool, get_dialog_keyboard, set_dialog_keyboard, reset_dialog_keyboard)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = MDILine()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
403
lib/python/qtvcp/widgets/mdi_touchy.py
Normal file
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp versa probe
|
||||
#
|
||||
# Copyright (c) 2018 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# touchy style MDI based heavily from Touchy code
|
||||
|
||||
import os
|
||||
|
||||
from qtpy import QtCore, QtWidgets, uic
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info, Path
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
PATH = Path()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
class mdi:
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
self.axes = []
|
||||
self.polar = 0
|
||||
axisnames = ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W']
|
||||
|
||||
try:
|
||||
STATUS.stat.poll()
|
||||
am = STATUS.stat.axis_mask
|
||||
for i in range(9):
|
||||
if am & (1<<i):
|
||||
self.axes.append(axisnames[i])
|
||||
except:
|
||||
self.axes = ['X','Y','Z']
|
||||
|
||||
self.gcode = 'M2'
|
||||
if INFO.MACHINE_IS_LATHE:
|
||||
G10 = ['Setup', 'L', 'P', 'A','R', 'I','J', 'Q']
|
||||
else:
|
||||
G10 = ['Setup', 'L', 'P', 'A', 'R']
|
||||
self.codes = {
|
||||
'M3' : ['Spindle CW', 'S'],
|
||||
'M4' : ['Spindle CCW', 'S'],
|
||||
'M6' : ['Tool change', 'T'],
|
||||
'M61' : ['Set tool number', 'Q'],
|
||||
'M66' : ['Input control', 'P', 'E', 'L', 'Q'],
|
||||
|
||||
# 'A' means 'the axes'
|
||||
'G0' : ['Straight rapid', 'A'],
|
||||
'G00' : ['Straight rapid', 'A'],
|
||||
'G1' : ['Straight feed', 'A', 'F'],
|
||||
'G01' : ['Straight feed', 'A', 'F'],
|
||||
'G2' : ['Arc CW', 'A', 'I', 'J', 'K', 'R', 'P', 'F'],
|
||||
'G02' : ['Arc CW', 'A', 'I', 'J', 'K', 'R', 'P', 'F'],
|
||||
'G3' : ['Arc CCW', 'A', 'I', 'J', 'K', 'R', 'P', 'F'],
|
||||
'G03' : ['Arc CCW', 'A', 'I', 'J', 'K', 'R', 'P', 'F'],
|
||||
'G4' : ['Dwell', 'P'],
|
||||
'G04' : ['Dwell', 'P'],
|
||||
'G10': G10,
|
||||
'G33' : ['Spindle synchronized feed', 'A', 'K'],
|
||||
'G33.1' : ['Rigid tap', 'Z', 'K'],
|
||||
'G38.2' : ['Probe', 'A', 'F'],
|
||||
'G38.3' : ['Probe', 'A', 'F'],
|
||||
'G38.4' : ['Probe', 'A', 'F'],
|
||||
'G38.5' : ['Probe', 'A', 'F'],
|
||||
'G41' : ['Radius compensation left', 'D'],
|
||||
'G42' : ['Radius compensation right', 'D'],
|
||||
'G41.1' : ['Radius compensation left, immediate', 'D', 'L'],
|
||||
'G42.1' : ['Radius compensation right, immediate', 'D', 'L'],
|
||||
'G43' : ['Tool length offset', 'H'],
|
||||
'G43.1' : ['Tool length offset immediate', 'A'],
|
||||
'G43.2' : ['Tool length offset additional', 'H', 'A'],
|
||||
'G53' : ['Motion in unoffset coordinates', 'G', 'A', 'F'],
|
||||
'G64' : ['Continuous mode', 'P', 'Q'],
|
||||
'G76' : ['Thread', 'Z', 'P', 'I', 'J', 'K', 'R', 'Q', 'H', 'E', 'L'],
|
||||
'G81' : ['Drill', 'A', 'R', 'L', 'F'],
|
||||
'G82' : ['Drill with dwell', 'A', 'R', 'L', 'P', 'F'],
|
||||
'G83' : ['Peck drill', 'A', 'R', 'L', 'Q', 'F'],
|
||||
'G73' : ['Chip-break drill', 'A', 'R', 'L', 'Q', 'F'],
|
||||
'G85' : ['Bore', 'A', 'R', 'L', 'F'],
|
||||
'G89' : ['Bore with dwell', 'A', 'R', 'L', 'P', 'F'],
|
||||
'G92' : ['Offset all coordinate systems', 'A'],
|
||||
'G96' : ['CSS Mode', 'S', 'D'],
|
||||
}
|
||||
self.ocodes = []
|
||||
|
||||
def add_macros(self, macros):
|
||||
for m in macros:
|
||||
words = m.split()
|
||||
call = "O<%s> call" % words[0]
|
||||
args = [''] + [w + ' ' for w in words[1:]]
|
||||
self.ocodes.append(call)
|
||||
self.codes[call] = args
|
||||
|
||||
def get_description(self, gcode):
|
||||
return self.codes[gcode][0]
|
||||
|
||||
def get_words(self, gcode):
|
||||
self.gcode = gcode
|
||||
try:
|
||||
if gcode[0] == 'M' and gcode.find(".") == -1 and int(gcode[1:]) >= 100 and int(gcode[1:]) <= 199:
|
||||
return ['P', 'Q']
|
||||
except IndexError:
|
||||
return []
|
||||
if gcode not in self.codes:
|
||||
return []
|
||||
# strip description
|
||||
words = self.codes[gcode][1:]
|
||||
# replace A with the real axis names
|
||||
if 'A' in words:
|
||||
i = words.index('A')
|
||||
words = words[:i] + self.axes + words[i+1:]
|
||||
if self.polar and 'X' in self.axes and 'Y' in self.axes:
|
||||
words[self.axes.index('X')] = '@'
|
||||
words[self.axes.index('Y')] = '^'
|
||||
return words
|
||||
|
||||
def clear(self):
|
||||
self.words = {}
|
||||
|
||||
def set_word(self, word, value):
|
||||
self.words[word] = value
|
||||
|
||||
def set_polar(self, p):
|
||||
self.polar = p;
|
||||
|
||||
def issue(self):
|
||||
m = self.gcode
|
||||
if m.lower().startswith('o'):
|
||||
codes = self.codes[m]
|
||||
for code in self.codes[m][1:]:
|
||||
v = self.words[code] or "0"
|
||||
m = m + " [%s]" % v
|
||||
else:
|
||||
w = [i for i in self.words if len(self.words.get(i)) > 0]
|
||||
if '@' in w:
|
||||
m += '@' + self.words.get('@')
|
||||
w.remove('@')
|
||||
if '^' in w:
|
||||
m += '^' + self.words.get('^')
|
||||
w.remove('^')
|
||||
for i in w:
|
||||
if len(self.words.get(i)) > 0:
|
||||
m += i + self.words.get(i)
|
||||
ACTION.CALL_MDI(m)
|
||||
try:
|
||||
fp = os.path.expanduser(INFO.MDI_HISTORY_PATH)
|
||||
fp = open(fp, 'a')
|
||||
fp.write(m + "\n")
|
||||
fp.close()
|
||||
except:
|
||||
pass
|
||||
STATUS.emit('mdi-history-changed')
|
||||
|
||||
class MDITouchy(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(MDITouchy, self).__init__(parent)
|
||||
self.setMinimumSize(265, 325)
|
||||
# Load the widgets UI file:
|
||||
self.filename = os.path.join(PATH.SHAREDIR,'widgets_ui', 'mdi_touchy.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
|
||||
self.mdi = mdi()
|
||||
self.numlabels = 11
|
||||
self.numwords = 1
|
||||
self.selected = 0
|
||||
self.plaintext = ['','','','','','','','','','','','']
|
||||
self.dColor = self.label_0.palette().button().color()
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_test()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
STATUS.connect('general',self.return_value)
|
||||
macros = INFO.INI_MACROS
|
||||
if len(macros) > 0:
|
||||
self.mdi.add_macros(macros)
|
||||
else:
|
||||
self.pushButton_macro.setEnabled(0)
|
||||
|
||||
def gxClicked(self):
|
||||
self.update("G")
|
||||
|
||||
def grClicked(self):
|
||||
self.update("G", 1)
|
||||
|
||||
def mClicked(self):
|
||||
self.update("M")
|
||||
|
||||
def tClicked(self):
|
||||
self.update("T")
|
||||
|
||||
def pointClicked(self):
|
||||
t = self.get_text()
|
||||
if t.find(".") == -1:
|
||||
self.set_text(t + ".")
|
||||
|
||||
def minusClicked(self):
|
||||
t = self.get_text()
|
||||
if self.selected > 0:
|
||||
head = t.rstrip("0123456789.-")
|
||||
tail = t[len(head):]
|
||||
if tail.find("-") == -1:
|
||||
self.set_text(head + "-" + tail)
|
||||
else:
|
||||
self.set_text(head + tail[1:])
|
||||
|
||||
def nextClicked(self):
|
||||
self.fill_out();
|
||||
if self.numwords > 0:
|
||||
self.editing(max(1,(self.selected+1) % (self.numwords+1)))
|
||||
|
||||
def pointClicked(self):
|
||||
t = self.get_text()
|
||||
if t.find(".") == -1:
|
||||
self.set_text(t + ".")
|
||||
|
||||
def clearClicked(self):
|
||||
t = self.get_text()
|
||||
self.set_text(t.rstrip("0123456789.-"))
|
||||
|
||||
def backClicked(self):
|
||||
t = self.get_text()
|
||||
if t[-1:] in "0123456789.-":
|
||||
self.set_text(t[:-1])
|
||||
|
||||
def numKeyClicked(self):
|
||||
t = self.get_text()
|
||||
sending_button = self.sender()
|
||||
num = sending_button.objectName().strip('pushButton_')
|
||||
self.set_text(t + num)
|
||||
|
||||
def setToolClicked(self):
|
||||
tool = STATUS.stat.tool_in_spindle
|
||||
self.set_tool(tool,True)
|
||||
|
||||
def setOriginClicked(self):
|
||||
self.set_origin()
|
||||
|
||||
def macroClicked(self):
|
||||
self.cycle_ocodes()
|
||||
|
||||
def calcClicked(self):
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'MDI Calculator'}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
#######################################
|
||||
#
|
||||
#######################################
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
t = self.get_text().rstrip("0123456789.-")
|
||||
self.set_text('{}{}'.format(t, num))
|
||||
STATUS.emit('update-machine-log', 'Calculation from MDI {}:'.format(num), 'TIME,DEBUG')
|
||||
|
||||
def run_command(self):
|
||||
self.fill_out()
|
||||
self.mdi.issue()
|
||||
|
||||
def update(self, code="G", polar=0):
|
||||
self.set_text('G', self.selected)
|
||||
self.mdi.set_polar(polar)
|
||||
self.set_text(code, 0)
|
||||
for i in range(1, self.numlabels):
|
||||
self.set_text("", i)
|
||||
self.editing(0)
|
||||
self.mdi.clear()
|
||||
|
||||
def fill_out(self):
|
||||
if self.selected == 0:
|
||||
w = self.mdi.get_words(self.get_text())
|
||||
self.numwords = len(w)
|
||||
for i in range(1,self.numlabels):
|
||||
if i <= len(w):
|
||||
self.set_text(w[i-1], i)
|
||||
else:
|
||||
self.set_text("", i)
|
||||
|
||||
def not_editing(self, n):
|
||||
#self['label_{}'.format(n)].setEnabled(False)
|
||||
self['label_{}'.format(n)].setStyleSheet("background-color:();".format( self.dColor.name()))
|
||||
|
||||
def editing(self, n):
|
||||
self.not_editing(self.selected)
|
||||
self.selected = n
|
||||
#self['label_{}'.format(n)].setEnabled(True)
|
||||
self['label_{}'.format(n)].setStyleSheet("background-color:#ffffff;")
|
||||
|
||||
def get_text(self):
|
||||
return self.plaintext[self.selected]
|
||||
|
||||
def set_text(self, t, n = -1):
|
||||
if n == -1: n = self.selected
|
||||
w = self['label_{}'.format(n)]
|
||||
w.setText('<b>{}</b>'.format(t))
|
||||
self.plaintext[n] = t
|
||||
if n > 0:
|
||||
head = t.rstrip("0123456789.-")
|
||||
tail = t[len(head):]
|
||||
self.mdi.set_word(head, tail)
|
||||
if len(t) < 2:
|
||||
w.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
else:
|
||||
w.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
|
||||
|
||||
def cycle_ocodes(self):
|
||||
# strip off richText bold encoding
|
||||
doc = self.label_0.text().lstrip('<b>')
|
||||
old_code = doc.rstrip('</b>')
|
||||
ocodes = self.mdi.ocodes
|
||||
if old_code in ocodes:
|
||||
j = (ocodes.index(old_code) + 1) % len(ocodes)
|
||||
else:
|
||||
j = 0
|
||||
self.update(ocodes[j])
|
||||
self.nextClicked()
|
||||
|
||||
def set_tool(self, tool, g10l11):
|
||||
self.update()
|
||||
self.set_text("G10", 0)
|
||||
self.nextClicked()
|
||||
if g10l11:
|
||||
self.set_text("L11", 1)
|
||||
else:
|
||||
self.set_text("L10", 1)
|
||||
self.nextClicked()
|
||||
self.set_text("P%d" % tool, 2)
|
||||
self.nextClicked() # go to first axis
|
||||
if ('X' in self.mdi.axes and
|
||||
'Y' in self.mdi.axes and
|
||||
'Z' in self.mdi.axes):
|
||||
# this is fairly mill-like, so go to Z
|
||||
self.nextClicked()
|
||||
self.nextClicked()
|
||||
|
||||
def set_origin(self, system=0):
|
||||
self.update()
|
||||
self.set_text("G10", 0)
|
||||
self.nextClicked()
|
||||
self.set_text("L20", 1)
|
||||
self.nextClicked()
|
||||
self.set_text("P%d" % system, 2)
|
||||
self.nextClicked()
|
||||
if ('X' in self.mdi.axes and
|
||||
'Z' in self.mdi.axes and
|
||||
not 'Y' in self.mdi.axes):
|
||||
# this is fairly lathe-like, so go to Z
|
||||
self.nextClicked()
|
||||
|
||||
########################################
|
||||
# required boiler code
|
||||
########################################
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
####################################
|
||||
# Testing
|
||||
####################################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
import sys
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = MDITouchy()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
306
lib/python/qtvcp/widgets/nurbs_editor.py
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
#############################################################################
|
||||
##
|
||||
## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>.
|
||||
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
|
||||
## All rights reserved.
|
||||
##
|
||||
## This file is part of the examples of PyQt.
|
||||
##
|
||||
## $QT_BEGIN_LICENSE:BSD$
|
||||
## You may use this file under the terms of the BSD license as follows:
|
||||
##
|
||||
## "Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted provided that the following conditions are
|
||||
## met:
|
||||
## * Redistributions of source code must retain the above copyright
|
||||
## notice, this list of conditions and the following disclaimer.
|
||||
## * Redistributions in binary form must reproduce the above copyright
|
||||
## notice, this list of conditions and the following disclaimer in
|
||||
## the documentation and/or other materials provided with the
|
||||
## distribution.
|
||||
## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
|
||||
## the names of its contributors may be used to endorse or promote
|
||||
## products derived from this software without specific prior written
|
||||
## permission.
|
||||
##
|
||||
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
## $QT_END_LICENSE$
|
||||
##
|
||||
###########################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Slot, QFile, QTextStream
|
||||
from qtpy.QtWidgets import (QApplication, QDialog)
|
||||
|
||||
import gcode
|
||||
from qt5_graphics import Lcnc_3dGraphics
|
||||
from qtvcp.core import Info, Path, Action
|
||||
|
||||
from qtvcp import logger
|
||||
LOG = logger.getLogger(__name__)
|
||||
#LOG = logger.initBaseLogger('QTvcp1', log_file=None, log_level=logger.DEBUG)
|
||||
|
||||
INFO = Info()
|
||||
PATH = Path()
|
||||
ACTION = Action()
|
||||
DATADIR = os.path.abspath( os.path.dirname( __file__ ) )
|
||||
|
||||
class NurbsEditor(QDialog):
|
||||
def __init__(self, parent=None, path=None):
|
||||
super(NurbsEditor, self).__init__(parent)
|
||||
self.setMinimumSize(500, 500)
|
||||
self.bluck_update = True
|
||||
|
||||
# Load the widgets UI file:
|
||||
self.filename = os.path.join(PATH.SHAREDIR,'widgets_ui', 'nurbs_editor.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
#exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
print()
|
||||
print("Ui loadinr error",formatted_lines[0])
|
||||
#traceback.print_tb(exc_traceback, limit=1, file=sys.stdout)
|
||||
print(formatted_lines[-1])
|
||||
if 'slotname' in formatted_lines[-2]:
|
||||
LOG.critical('Missing slot name {}'.format(e))
|
||||
else:
|
||||
LOG.critical(e)
|
||||
|
||||
self.setWindowTitle('NurbsEditor Dialog');
|
||||
|
||||
|
||||
self.workpath = os.path.join( "/tmp/LINUXCNCtempWork.ngc")
|
||||
self.emptypath = os.path.join( "/tmp/LINUXCNCtempEmpty.ngc")
|
||||
|
||||
emptyfile = open(self.emptypath, "w")
|
||||
print(("m2"), file=emptyfile)
|
||||
emptyfile.close()
|
||||
|
||||
self.set_spinbuttons_default()
|
||||
self.graphics = Lcnc_3dGraphics()
|
||||
|
||||
self.setGraphicsDisplay()
|
||||
self.main.addWidget(self.graphics)
|
||||
self.updateDisplay(self.emptypath)
|
||||
self.update()
|
||||
|
||||
def setGraphicsDisplay(self):
|
||||
# class patch to catch gcode errors - in theory
|
||||
self.graphics.report_gcode_error = self.report_gcode_error
|
||||
# reset trverse color so other displays don;t change
|
||||
self.defaultColor = self.graphics.colors['traverse']
|
||||
self.graphics.current_view = 'z'
|
||||
self.graphics.metric_units = INFO.MACHINE_IS_METRIC
|
||||
self.graphics.use_gradient_background = True
|
||||
self.graphics.show_tool = False
|
||||
self.graphics.grid_size = 2
|
||||
self.graphics.cancel_rotate = True
|
||||
|
||||
def set_spinbuttons_default(self):
|
||||
self.use_ctrl1.setChecked(True)
|
||||
self.use_ctrl2.setChecked(True)
|
||||
self.use_ctrl3.setChecked(True)
|
||||
self.use_ctrl4.setChecked(True)
|
||||
|
||||
self.x1.setValue(3.53)
|
||||
self.x2.setValue(5.53)
|
||||
self.x3.setValue(3.52)
|
||||
self.x4.setValue(0)
|
||||
|
||||
self.y1.setValue(-1.50)
|
||||
self.y2.setValue(-11.01)
|
||||
self.y3.setValue(-24.00)
|
||||
self.y4.setValue(-29.56)
|
||||
|
||||
self.w1.setValue(2)
|
||||
self.w2.setValue(1)
|
||||
self.w3.setValue(1)
|
||||
self.w4.setValue(1)
|
||||
self.gridSpace.setValue(2)
|
||||
|
||||
self.unblock()
|
||||
|
||||
def checkChanged(self):
|
||||
c = 0
|
||||
for i in range(1,8):
|
||||
u = 'use_ctrl{}'.format(i)
|
||||
if self[u].isChecked(): c +=1
|
||||
if c < 2:
|
||||
self.sender().setChecked(True)
|
||||
elif not self.bluck_update:
|
||||
self.update()
|
||||
|
||||
def spinChanged(self,data):
|
||||
if self.bluck_update: return
|
||||
self.update()
|
||||
|
||||
def gridSpinChanged(self):
|
||||
if self.bluck_update: return
|
||||
self.graphics.grid_size = self.gridSpace.value()
|
||||
self.graphics.updateGL()
|
||||
|
||||
def block(self):
|
||||
self.bluck_update = True
|
||||
def unblock(self):
|
||||
self.bluck_update = False
|
||||
|
||||
def invertX(self):
|
||||
self.block()
|
||||
for i in range(1,8):
|
||||
X = 'x{}'.format(i)
|
||||
self[X].setValue(self[X].value() * -1)
|
||||
self.rapidX.setValue(self.rapidX.value() * -1)
|
||||
self.unblock()
|
||||
self.update()
|
||||
|
||||
def invertY(self):
|
||||
self.block()
|
||||
for i in range(1,8):
|
||||
Y = 'y{}'.format(i)
|
||||
self[Y].setValue(self[Y].value() * -1)
|
||||
self.rapidY.setValue(self.rapidY.value() * -1)
|
||||
self.unblock()
|
||||
self.update()
|
||||
|
||||
def resetView(self):
|
||||
self.graphics.set_current_view()
|
||||
|
||||
def update(self):
|
||||
self.graphics.colors['traverse'] = (0.0, 1.0, 0.0)
|
||||
self.workfile = open(self.workpath, "w")
|
||||
|
||||
###############################
|
||||
# Setup
|
||||
###############################
|
||||
t = self.toolNum.value()
|
||||
if t:
|
||||
print(('T{} M6 G43').format(self.toolNum.value()), file=self.workfile)
|
||||
print(('( Starting position )'), file=self.workfile)
|
||||
print(('G0 X {} Y {} Z {}').format(self.rapidX.value(),self.rapidY.value(),self.rapidZ.value()), file=self.workfile)
|
||||
|
||||
###############################
|
||||
# Nurbs block
|
||||
###############################
|
||||
print((''), file=self.workfile)
|
||||
print(('( Nurbs block )'), file=self.workfile)
|
||||
f = self.feedRate.value()
|
||||
if f:
|
||||
print(('F{}').format(self.feedRate.value()), file=self.workfile)
|
||||
#print >> self.workfile, ('F')
|
||||
print(('G5.2'), file=self.workfile)
|
||||
for i in range(1,8):
|
||||
u = 'use_ctrl{}'.format(i)
|
||||
X = 'x{}'.format(i)
|
||||
Y = 'y{}'.format(i)
|
||||
P = 'w{}'.format(i)
|
||||
if self[u].isChecked():
|
||||
print(('X {} Y {} P {}').format(self[X].value(),self[Y].value(),self[P].value()), file=self.workfile)
|
||||
print(('G5.3'), file=self.workfile)
|
||||
|
||||
##############################
|
||||
# rapids to show boundary
|
||||
##############################
|
||||
print((''), file=self.workfile)
|
||||
print(('( Show control points with rapid lines )'), file=self.workfile)
|
||||
print(('G0 X {} Y {} Z {}').format(self.rapidX.value(),self.rapidY.value(),self.rapidZ.value()), file=self.workfile)
|
||||
for i in range(1,8):
|
||||
u = 'use_ctrl{}'.format(i)
|
||||
X = 'x{}'.format(i)
|
||||
Y = 'y{}'.format(i)
|
||||
if self[u].isChecked():
|
||||
print(('G0 X{} y{}').format(self[X].value(),self[Y].value()), file=self.workfile)
|
||||
|
||||
##############################
|
||||
# cleanup
|
||||
##############################
|
||||
print(('m2'), file=self.workfile)
|
||||
|
||||
self.workfile.close()
|
||||
self.updateDisplay(self.workpath)
|
||||
self.graphics.colors['traverse'] = self.defaultColor
|
||||
|
||||
def finalizeGcode(self):
|
||||
gcode = self.gcodeText.toPlainText()
|
||||
file = QFile(self.workpath)
|
||||
if file.open(QFile.WriteOnly):
|
||||
QTextStream(file) << gcode
|
||||
|
||||
def updateDisplay(self,fn):
|
||||
self.graphics.load(fn)
|
||||
|
||||
def load_dialog(self):
|
||||
#self.updateDisplay(self.emptypath)
|
||||
self.graphics.current_view = 'z'
|
||||
self.show() # must be realized before view will change
|
||||
self.graphics.set_current_view()
|
||||
#self.activateWindow()
|
||||
|
||||
@Slot()
|
||||
def on_makeButton_clicked(self):
|
||||
print('make')
|
||||
file = QFile(self.workpath)
|
||||
file.open(QFile.ReadOnly)
|
||||
gcode = file.readAll()
|
||||
try:
|
||||
# Python v2.
|
||||
gcode = str(gcode, encoding='utf8')
|
||||
except NameError:
|
||||
# Python v3.
|
||||
gcode = str(gcode, encoding='utf8')
|
||||
self.gcodeText.setPlainText(gcode)
|
||||
|
||||
@Slot()
|
||||
def on_applyButton_clicked(self):
|
||||
self.finalizeGcode()
|
||||
ACTION.OPEN_PROGRAM(self.workpath)
|
||||
|
||||
@Slot()
|
||||
def on_closeButton_clicked(self):
|
||||
self.close()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
|
||||
###############
|
||||
# class patch
|
||||
############
|
||||
def report_gcode_error(self, result, seq, filename):
|
||||
error_str = gcode.strerror(result)
|
||||
errortext = "G-Code error in " + os.path.basename(filename) + "\n" + "Near line " \
|
||||
+ str(seq) + " of\n" + filename + "\n" + error_str + "\n"
|
||||
print(errortext)
|
||||
|
||||
###########
|
||||
# Testing
|
||||
###########
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
if len(sys.argv) == 1:
|
||||
inifilename = None
|
||||
elif len(sys.argv) == 2:
|
||||
inifilename = sys.argv[1]
|
||||
else:
|
||||
sys.exit(1)
|
||||
window = NurbsEditor(path = inifilename)
|
||||
window.load_dialog()
|
||||
sys.exit(app.exec())
|
||||
|
||||
210
lib/python/qtvcp/widgets/offset_tool_button.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget
|
||||
# Copyright (c) 2019 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.
|
||||
###############################################################################
|
||||
|
||||
from qtpy.QtWidgets import QToolButton, QMenu, QAction
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from LinuxCNC
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds INI details
|
||||
# ACTION gives commands to LinuxCNC
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class OffsetToolButton(QToolButton, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(OffsetToolButton, self).__init__(parent)
|
||||
self._axis = ''
|
||||
self._last = 0
|
||||
self._block_signal = False
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.display_units_mm = 0
|
||||
|
||||
self.settingMenu = QMenu(self)
|
||||
self.setMenu(self.settingMenu)
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), 'Set Current Tool Position', self)
|
||||
Button.triggered.connect(self.setOffset)
|
||||
self.settingMenu.addAction(Button)
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), 'Adjust Current Tool Position', self)
|
||||
Button.triggered.connect(self.adjustOffset)
|
||||
self.settingMenu.addAction(Button)
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), 'Zero Current Tool Position', self)
|
||||
Button.triggered.connect(self.zeroOffset)
|
||||
self.settingMenu.addAction(Button)
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), 'Set Tool Offset Directly', self)
|
||||
Button.triggered.connect(self.setDirectOffset)
|
||||
self.settingMenu.addAction(Button)
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), 'Reset To Last', self)
|
||||
Button.triggered.connect(self.last)
|
||||
self.settingMenu.addAction(Button)
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on()
|
||||
and (STATUS.get_current_tool() not in(0,-1))
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED))
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('state-off', lambda w: self.settingMenu.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.settingMenu.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.settingMenu.setEnabled(homed_on_test()))
|
||||
STATUS.connect('interp-run', lambda w: self.settingMenu.setEnabled(False))
|
||||
STATUS.connect('all-homed', lambda w: self.settingMenu.setEnabled(homed_on_test()))
|
||||
STATUS.connect('not-all-homed', lambda w, data: self.settingMenu.setEnabled(False))
|
||||
STATUS.connect('interp-paused', lambda w: self.settingMenu.setEnabled(homed_on_test()))
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w,t:self.settingMenu.setEnabled(homed_on_test()))
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
self._last = message.get('CURRENT')
|
||||
axis = message.get('AXIS')
|
||||
fixture = message.get('FIXTURE') or False
|
||||
direct = message.get('DIRECT') or False
|
||||
adjust = message.get('ADJUST') or False
|
||||
if direct:
|
||||
if adjust:
|
||||
num = self._last - num
|
||||
ACTION.SET_DIRECT_TOOL_OFFSET(axis,num)
|
||||
STATUS.emit('update-machine-log', 'Set Direct tool offset of Axis {}: {} to {}'.format(axis, self._last, num), 'TIME,SUCCESS')
|
||||
else:
|
||||
ACTION.SET_TOOL_OFFSET(axis,num,fixture)
|
||||
STATUS.emit('update-machine-log', 'Adjust Tool offset of Axis {}, {} to {}'.format(axis, self._last, num), 'TIME,SUCCESS')
|
||||
|
||||
def zeroOffset(self):
|
||||
axis, tool, now = self._get_current_info(self._axis)
|
||||
if axis:
|
||||
self._last = now
|
||||
fixture = False
|
||||
ACTION.SET_TOOL_OFFSET(axis, 0, fixture)
|
||||
STATUS.emit('update-machine-log', 'Zeroed Tool Offset {} of Tool {}'.format(axis, tool), 'TIME,SUCCESS')
|
||||
|
||||
def setOffset(self):
|
||||
axis, tool, now = self._get_current_info(self._axis)
|
||||
if axis:
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,'CURRENT':now,
|
||||
'TITLE':'Set Axis {} Current Position by setting Offset of Tool {}'.format(axis, tool)}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
def adjustOffset(self):
|
||||
axis, tool, now = self._get_current_info(self._axis)
|
||||
if axis:
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,'CURRENT':now,'ADJUST':True, 'DIRECT':True,
|
||||
'TITLE':'Adjust Axis {} Current Position by Offsetting Tool {} by Entered Amount'.format(axis, tool, now)}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
def setDirectOffset(self):
|
||||
axis, tool, now = self._get_current_info(self._axis)
|
||||
if axis:
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,'CURRENT':now,'DIRECT':True,
|
||||
'TITLE':'Set Axis {} Offset of Tool {}'.format(axis, tool)}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
def last(self):
|
||||
axis, tool, now = self._get_current_info(self._axis)
|
||||
if axis:
|
||||
fixture = False
|
||||
ACTION.SET_DIRECT_TOOL_OFFSET(axis, self._last)
|
||||
STATUS.emit('update-machine-log', 'Reset Tool {} Offset To Last {}'.format(tool, self._last), 'TIME,SUCCESS')
|
||||
self._last = now
|
||||
|
||||
def _get_current_info(self, axis):
|
||||
tool = STATUS.get_current_tool()
|
||||
if STATUS.is_joint_mode() or not tool:
|
||||
return None, None, None
|
||||
if axis == '':
|
||||
axis = STATUS.get_selected_axis()
|
||||
j = "XYZABCUVW"
|
||||
try:
|
||||
jnum = j.find(axis)
|
||||
except:
|
||||
return None, None, None
|
||||
offset = STATUS.stat.tool_offset[jnum]
|
||||
return axis, tool, offset
|
||||
|
||||
def _switch_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
self._last = INFO.convert_units(self._last)
|
||||
|
||||
#########################################################################
|
||||
# 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 set_axis(self, data):
|
||||
if data.upper() in('X','Y','Z','A','B','C','U','V','W'):
|
||||
self._axis = str(data.upper())
|
||||
def get_axis(self):
|
||||
return self._axis
|
||||
def reset_axis(self):
|
||||
self._axis = ''
|
||||
axis_letter = Property(str, get_axis, set_axis, reset_axis)
|
||||
|
||||
|
||||
def set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialog_code_string = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = OffsetToolButton()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
217
lib/python/qtvcp/widgets/operator_value_line.py
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget - Operator-entered value line edit widget
|
||||
# optional dialog entry and optional MDI command issuing
|
||||
#
|
||||
# Copyright (c) 2025 Steve Richardson
|
||||
#
|
||||
# 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 os
|
||||
import linuxcnc
|
||||
import hal
|
||||
import time
|
||||
|
||||
import subprocess
|
||||
|
||||
from qtpy.QtWidgets import QLineEdit, QApplication
|
||||
from qtpy.QtCore import Qt, QEvent, Property
|
||||
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp.widgets.entry_widget import SoftInputWidget
|
||||
from qtvcp.lib.aux_program_loader import Aux_program_loader
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
AUX_PRGM = Aux_program_loader()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
class OperatorValue(QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
super(OperatorValue, self).__init__(parent)
|
||||
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(not STATUS.is_auto_mode()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(STATUS.machine_is_on()))
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
class OperatorValueLine(OperatorValue):
|
||||
def __init__(self, parent=None):
|
||||
super(OperatorValueLine, self).__init__(parent)
|
||||
self._PARENT_WIDGET = parent
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.soft_keyboard = False
|
||||
self.dialog_keyboard = False
|
||||
self.issue_mdi_on_submit = False
|
||||
self.issue_mdi_on_return = False
|
||||
self.mdi_command_format = "M3 S{value}"
|
||||
self.pending_value = False
|
||||
self._input_panel_full = SoftInputWidget(self, 'default')
|
||||
self.installEventFilter(self)
|
||||
|
||||
self.returnPressed.connect(self.handleReturnKey)
|
||||
|
||||
def handleReturnKey(self):
|
||||
self.setFocus()
|
||||
self.setCursorPosition(len(self.text())+1)
|
||||
if self.issue_mdi_on_return:
|
||||
self.pending_value = False
|
||||
self._style_polish('isPendingValue', self.pending_value)
|
||||
self.submit(self.mdi_command_format)
|
||||
|
||||
def submit(self, mdi_format):
|
||||
value = str(self.text()).strip()
|
||||
if value == '': return
|
||||
namespace = {'value': value}
|
||||
|
||||
ACTION.CALL_MDI(mdi_format.format(**namespace) + "\n")
|
||||
|
||||
# try/except is so designer will load
|
||||
def eventFilter(self, widget, event):
|
||||
if self.focusWidget() == widget and event.type() == QEvent.MouseButtonPress:
|
||||
if self.soft_keyboard:
|
||||
self._input_panel_full.show_input_panel(widget)
|
||||
elif self.dialog_keyboard:
|
||||
self.request_keyboard()
|
||||
return False
|
||||
|
||||
def request_keyboard(self):
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'TITLE':'Operator Value',
|
||||
'GEONAME':'OperatorValue_Keyboard_Dialog_{}'.format(self.dialog_code),
|
||||
}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
value = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and value is not None:
|
||||
self.setFocus()
|
||||
self.setText(f"{value:.0f}")
|
||||
self.setCursorPosition(len(self.text())+1)
|
||||
if self.issue_mdi_on_submit:
|
||||
self.pending_value = False
|
||||
self._style_polish('isPendingValue', self.pending_value)
|
||||
self.submit(self.mdi_command_format)
|
||||
else:
|
||||
self.pending_value = True
|
||||
self._style_polish('isPendingValue', self.pending_value)
|
||||
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
|
||||
def issue_mdi(self):
|
||||
self.submit(self.mdi_command_format)
|
||||
self.pending_value = False
|
||||
self._style_polish('isPendingValue', self.pending_value)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#########################################################################
|
||||
|
||||
# force style sheet to update
|
||||
def _style_polish(self, prop, state):
|
||||
self.setProperty(prop, state)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
def set_issue_mdi_on_submit(self, data):
|
||||
self.issue_mdi_on_submit = data
|
||||
def get_issue_mdi_on_submit(self):
|
||||
return self.issue_mdi_on_submit
|
||||
def reset_issue_mdi_on_submit(self):
|
||||
self.issue_mdi_on_submit = False
|
||||
|
||||
issue_mdi_on_submit_option = Property(bool, get_issue_mdi_on_submit, set_issue_mdi_on_submit, reset_issue_mdi_on_submit)
|
||||
|
||||
def set_issue_mdi_on_return(self, data):
|
||||
self.issue_mdi_on_return = data
|
||||
def get_issue_mdi_on_return(self):
|
||||
return self.issue_mdi_on_return
|
||||
def reset_issue_mdi_on_return(self):
|
||||
self.issue_mdi_on_return = False
|
||||
|
||||
issue_mdi_on_return_option = Property(bool, get_issue_mdi_on_return, set_issue_mdi_on_return, reset_issue_mdi_on_return)
|
||||
|
||||
def set_mdi_command_format(self, data):
|
||||
self.mdi_command_format = data
|
||||
def get_mdi_command_format(self):
|
||||
return self.mdi_command_format
|
||||
def reset_mdi_command_format(self):
|
||||
self.mdi_command_format = False
|
||||
|
||||
mdi_command_format_option = Property(str, get_mdi_command_format, set_mdi_command_format, reset_mdi_command_format)
|
||||
|
||||
|
||||
def set_soft_keyboard(self, data):
|
||||
self.soft_keyboard = data
|
||||
def get_soft_keyboard(self):
|
||||
return self.soft_keyboard
|
||||
def reset_soft_keyboard(self):
|
||||
self.soft_keyboard = False
|
||||
|
||||
soft_keyboard_option = Property(bool, get_soft_keyboard, set_soft_keyboard, reset_soft_keyboard)
|
||||
|
||||
def set_dialog_keyboard(self, data):
|
||||
self.dialog_keyboard = data
|
||||
def get_dialog_keyboard(self):
|
||||
return self.dialog_keyboard
|
||||
def reset_dialog_keyboard(self):
|
||||
self.dialog_keyboard = False
|
||||
|
||||
dialog_keyboard_option = Property(bool, get_dialog_keyboard, set_dialog_keyboard, reset_dialog_keyboard)
|
||||
|
||||
def set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = False
|
||||
|
||||
dialog_code_option = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_pending_value(self, data):
|
||||
self.pending_value = data
|
||||
def get_pending_value(self):
|
||||
return self.pending_value
|
||||
def reset_pending_value(self):
|
||||
self.pending_value = False
|
||||
|
||||
isPendingValue = Property(bool, get_pending_value, set_pending_value, reset_pending_value)
|
||||
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = OperatorValueLine()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
596
lib/python/qtvcp/widgets/origin_offsetview.py
Normal file
@@ -0,0 +1,596 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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 sys
|
||||
import os
|
||||
import locale
|
||||
|
||||
from qtpy.QtCore import Qt, QAbstractTableModel, Property, Slot
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QTableView, QAbstractItemView, QApplication
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
#BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))
|
||||
#LOCALEDIR = os.path.join(BASE, "share", "locale")
|
||||
#locale.setlocale(locale.LC_ALL, '')
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class OriginOffsetView(QTableView, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(OriginOffsetView, self).__init__(parent)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.filename = INFO.PARAMETER_FILE
|
||||
self.axisletters = ["x", "y", "z", "a", "b", "c", "u", "v", "w"]
|
||||
self.current_system = None
|
||||
self._system_int = 1
|
||||
self.current_tool = 0
|
||||
self.metric_display = False
|
||||
self.metric_text_template = '%10.3f'
|
||||
self.imperial_text_template = '%9.4f'
|
||||
self.setEnabled(False)
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.text_dialog_code = 'KEYBOARD'
|
||||
self.table = self.createTable()
|
||||
|
||||
def _hal_init(self):
|
||||
self.delay = 0
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
STATUS.connect('not-all-homed', lambda w, axis: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('periodic', self.periodic_check)
|
||||
STATUS.connect('metric-mode-changed', lambda w, data: self.metricMode(data))
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w, data: self.currentTool(data))
|
||||
STATUS.connect('user-system-changed', self._convert_system)
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
conversion = {0:"X", 1:"Y", 2:"Z", 3:"A", 4:"B", 5:"C", 6:"U", 7:"V", 8:"W"}
|
||||
for num, let in conversion.items():
|
||||
if let in (INFO.AVAILABLE_AXES):
|
||||
continue
|
||||
self.hideColumn(num)
|
||||
|
||||
# If there is a preference file object use it to load the hi/low toggle points
|
||||
if self.PREFS_:
|
||||
self.tabledata[4][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G54', 'User System 1', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[5][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G55', 'User System 2', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[6][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G56', 'User System 3', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[7][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G57', 'User System 4', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[8][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G58', 'User System 5', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[9][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G59', 'User System 6', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[10][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G59.1', 'User System 7', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[11][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G59.2', 'User System 8', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tabledata[12][10] = self.PREFS_.getpref(self.HAL_NAME_+'-G59.3', 'User System 9', str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.tablemodel.layoutChanged.emit()
|
||||
|
||||
# when qtvcp closes this gets called
|
||||
def _hal_cleanup(self):
|
||||
if self.PREFS_:
|
||||
LOG.debug('Saving {} data to file.'.format(self.HAL_NAME_))
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G54', self.tabledata[4][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G55', self.tabledata[5][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G56', self.tabledata[6][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G57', self.tabledata[7][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G58', self.tabledata[8][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G59', self.tabledata[9][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G59.1', self.tabledata[10][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G59.2', self.tabledata[11][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
self.PREFS_.putpref(self.HAL_NAME_+'-G59.3', self.tabledata[12][10], str, 'ORIGINOFFSET_SYSTEM_NAMES')
|
||||
|
||||
def _convert_system(self, w, data):
|
||||
convert = ("None", "G54", "G55", "G56", "G57", "G58", "G59", "G59.1", "G59.2", "G59.3")
|
||||
self.current_system = convert[int(data)]
|
||||
self._system_int = int(data)
|
||||
|
||||
def currentTool(self, data):
|
||||
self.current_tool = data
|
||||
def metricMode(self, state):
|
||||
self.metric_display = state
|
||||
|
||||
def createTable(self):
|
||||
# create blank taple array
|
||||
self.tabledata = [[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 'Absolute Position'],
|
||||
[None, None, 2, None, None, None, None, None, None, None, 'Rotational Offsets'],
|
||||
[0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 'G92 Offsets'],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Current Tool'],
|
||||
[0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 'System 1'],
|
||||
[0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 'System 2'],
|
||||
[0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 'System 3'],
|
||||
[0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 'System 4'],
|
||||
[0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 'System 5'],
|
||||
[0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 'System 6'],
|
||||
[0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 'System 7'],
|
||||
[0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 'System 8'],
|
||||
[0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 'System 9']]
|
||||
|
||||
# create the view
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
|
||||
# set the table model
|
||||
header = ['X', 'Y', 'Z', 'A', 'B', 'C', 'U', 'V', 'W', 'Rot', 'Name']
|
||||
vheader = ['ABS', 'Rot', 'G92', 'Tool', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3']
|
||||
self.tablemodel = MyTableModel(self.tabledata, header, vheader, self)
|
||||
self.setModel(self.tablemodel)
|
||||
self.clicked.connect(self.showSelection)
|
||||
#self.dataChanged.connect(self.selection_changed)
|
||||
|
||||
# set the minimum size
|
||||
self.setMinimumSize(100, 100)
|
||||
|
||||
# set horizontal header properties
|
||||
hh = self.horizontalHeader()
|
||||
hh.setStretchLastSection(True)
|
||||
hh.setMinimumSectionSize(75)
|
||||
|
||||
# set column width to fit contents
|
||||
self.resizeColumnsToContents()
|
||||
|
||||
# set row height
|
||||
self.resizeRowsToContents()
|
||||
|
||||
# enable sorting
|
||||
self.setSortingEnabled(False)
|
||||
|
||||
def showSelection(self, item):
|
||||
cellContent = item.data()
|
||||
#text = cellContent.toPyObject() # test
|
||||
text = cellContent
|
||||
LOG.debug('Text: {}, Row: {}, Column: {}'.format(text, item.row(), item.column()))
|
||||
sf = "You clicked on {}".format(text)
|
||||
# display in title bar for convenience
|
||||
self.setWindowTitle(sf)
|
||||
# row 0 is not editable (absolute position)
|
||||
# row 1 has limited entries (rotational)
|
||||
# column 10 is the descriptive text column
|
||||
if item.column() == 10:
|
||||
self.callTextDialog(text,item)
|
||||
elif item.row() == 1:
|
||||
if item.column() == 2:
|
||||
self.callDialog(text,item)
|
||||
elif item.row() in (2,3) and item.column() == 9:
|
||||
pass
|
||||
elif item.row() > 1:
|
||||
self.callDialog(text,item)
|
||||
|
||||
# alphanumerical
|
||||
def callTextDialog(self, text,item):
|
||||
text = self.tablemodel.arraydata[item.row()][10]
|
||||
system = self.tablemodel.Vheaderdata[item.row()]
|
||||
mess = {'NAME':self.text_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'PRELOAD':text, 'TITLE':'{} System Description Entry'.format(system),
|
||||
'ITEM':item}
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
def callDialog(self, text,item,next=False):
|
||||
axis = self.tablemodel.headerdata[item.column()]
|
||||
system = self.tablemodel.Vheaderdata[item.row()]
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'PRELOAD':locale.delocalize(text), 'TITLE':'{} Offset of {},{}'.format(system, axis,text),
|
||||
'ITEM':item,
|
||||
'NEXT':next,
|
||||
'WIDGETCYCLE': True}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
name2 = bool(message.get('NAME') == self.text_dialog_code)
|
||||
item = message.get('ITEM')
|
||||
next = message.get('NEXT', False)
|
||||
back = message.get('BACK', False)
|
||||
|
||||
if code:
|
||||
LOG.debug('message returned:{}'.format (message))
|
||||
if code and (name or name2) and num is not None:
|
||||
self.tablemodel.setData(item, num, None)
|
||||
self.tablemodel.layoutChanged.emit()
|
||||
if code and name:
|
||||
# request for next input widget from nextlist
|
||||
if next:
|
||||
self.right()
|
||||
|
||||
newobj = self.currentIndex()
|
||||
# if we selected the text column, move back
|
||||
if newobj.column() == 10:
|
||||
self.left()
|
||||
newobj = self.currentIndex()
|
||||
cellContent = newobj.data()
|
||||
text = cellContent
|
||||
|
||||
# update the screen
|
||||
QApplication.processEvents()
|
||||
|
||||
# update the dialog
|
||||
self.callDialog(text,newobj,True)
|
||||
elif back:
|
||||
self.left()
|
||||
|
||||
newobj = self.currentIndex()
|
||||
cellContent = newobj.data()
|
||||
text = cellContent
|
||||
|
||||
# update the screen
|
||||
QApplication.processEvents()
|
||||
|
||||
# update the dialog
|
||||
self.callDialog(text,newobj,True)
|
||||
|
||||
# This function uses the color name (string); setProperty
|
||||
# expects a QColor object
|
||||
def highlight(self, color):
|
||||
self.setProperty('styleColorHighlight', QColor(color))
|
||||
|
||||
#############################################################
|
||||
|
||||
# Reload the offsets into display
|
||||
def reload_offsets(self):
|
||||
|
||||
# read the var file so we get all of the user offsets.
|
||||
temp = self.read_file()
|
||||
|
||||
# overwrite with motion's version of the current system offset
|
||||
# this should be more up to date
|
||||
STATUS.stat.poll()
|
||||
temp[STATUS.stat.g5x_index-1] = list(STATUS.stat.g5x_offset)
|
||||
# inject the current rotation into the list
|
||||
temp[STATUS.stat.g5x_index-1].append(STATUS.stat.rotation_xy)
|
||||
|
||||
g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3 = temp
|
||||
if g54 is None: return
|
||||
|
||||
# fake if linuxcnc is not running
|
||||
if STATUS.is_status_valid() == False:
|
||||
self.current_system = "G54"
|
||||
self._system_int = 1
|
||||
|
||||
# Get the offsets arrays and convert the units if the display
|
||||
# is not in machine native units
|
||||
|
||||
ap = STATUS.stat.actual_position
|
||||
tool = STATUS.stat.tool_offset
|
||||
g92 = STATUS.stat.g92_offset
|
||||
rot = STATUS.stat.rotation_xy
|
||||
|
||||
if self.metric_display != INFO.MACHINE_IS_METRIC:
|
||||
ap = INFO.convert_units_10(ap)
|
||||
tool = INFO.convert_units_10(tool)
|
||||
g92 = INFO.convert_units_10(g92)
|
||||
g54 = INFO.convert_units_10(g54)
|
||||
g55 = INFO.convert_units_10(g55)
|
||||
g56 = INFO.convert_units_10(g56)
|
||||
g57 = INFO.convert_units_10(g57)
|
||||
g58 = INFO.convert_units_10(g58)
|
||||
g59 = INFO.convert_units_10(g59)
|
||||
g59_1 = INFO.convert_units_10(g59_1)
|
||||
g59_2 = INFO.convert_units_10(g59_2)
|
||||
g59_3 = INFO.convert_units_10(g59_3)
|
||||
|
||||
# set the text style based on unit type
|
||||
if self.metric_display:
|
||||
tmpl = self.metric_text_template
|
||||
else:
|
||||
tmpl = self.imperial_text_template
|
||||
|
||||
degree_tmpl = "%{}.2f".format(len(locale.format_string(tmpl, 0)))
|
||||
|
||||
# fill each row of the liststore from the offsets arrays
|
||||
for row, i in enumerate([ap, rot, g92, tool, g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3]):
|
||||
for column in range(0, 10):
|
||||
|
||||
# current rotational
|
||||
if row == 1:
|
||||
if column == 2:
|
||||
self.tabledata[row][column] = locale.format_string(degree_tmpl, rot)
|
||||
else:
|
||||
self.tabledata[row][column] = " "
|
||||
|
||||
# rotational column not available in row 0,2,3
|
||||
elif row in (0,2,3) and column == 9:
|
||||
self.tabledata[row][column] = " "
|
||||
|
||||
elif column == 9:
|
||||
self.tabledata[row][column] = locale.format_string(degree_tmpl, i[column])
|
||||
else:
|
||||
self.tabledata[row][column] = locale.format_string(tmpl, i[column])
|
||||
|
||||
self.tablemodel.layoutChanged.emit()
|
||||
|
||||
# We read the var file directly
|
||||
# and pull out the info we need
|
||||
# if anything goes wrong we set all the info to 0
|
||||
def read_file(self):
|
||||
try:
|
||||
g54 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g55 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g56 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g57 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g58 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g59 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g59_1 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g59_2 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
g59_3 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
if self.filename is None:
|
||||
return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3
|
||||
if not os.path.exists(self.filename):
|
||||
LOG.error('File does not exist: yellow<{}>'.format(self.filename))
|
||||
return g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3
|
||||
logfile = open(self.filename, "r").readlines()
|
||||
for line in logfile:
|
||||
temp = line.split()
|
||||
param = int(temp[0])
|
||||
data = float(temp[1])
|
||||
|
||||
if 5230 >= param >= 5221:
|
||||
g54[param - 5221] = data
|
||||
elif 5250 >= param >= 5241:
|
||||
g55[param - 5241] = data
|
||||
elif 5270 >= param >= 5261:
|
||||
g56[param - 5261] = data
|
||||
elif 5290 >= param >= 5281:
|
||||
g57[param - 5281] = data
|
||||
elif 5310 >= param >= 5301:
|
||||
g58[param - 5301] = data
|
||||
elif 5330 >= param >= 5321:
|
||||
g59[param - 5321] = data
|
||||
elif 5350 >= param >= 5341:
|
||||
g59_1[param - 5341] = data
|
||||
elif 5370 >= param >= 5361:
|
||||
g59_2[param - 5361] = data
|
||||
elif 5390 >= param >= 5381:
|
||||
g59_3[param - 5381] = data
|
||||
return [g54, g55, g56, g57, g58, g59, g59_1, g59_2, g59_3]
|
||||
except:
|
||||
return [None, None, None, None, None, None, None, None, None]
|
||||
|
||||
def dataChanged(self, new, old, x):
|
||||
row = new.row()
|
||||
col = new.column()
|
||||
data = self.tabledata[row][col]
|
||||
|
||||
if row == 0: return
|
||||
|
||||
# Hack to not edit any rotational offset but Z axis
|
||||
if row == 1 and not col == 2: return
|
||||
|
||||
if row in(2,3) and col == 9: return
|
||||
|
||||
# don't evaluate text column
|
||||
if col == 10 :return
|
||||
|
||||
# make sure we switch to correct units for machine and rotational, row 2, does not get converted
|
||||
try:
|
||||
qualified = float(data)
|
||||
#qualified = float(locale.atof(data))
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
# now update linuxcnc to the change
|
||||
try:
|
||||
if STATUS.is_status_valid():
|
||||
ACTION.RECORD_CURRENT_MODE()
|
||||
if row == 0: # current Origin
|
||||
ACTION.CALL_MDI("G10 L2 P0 %s %10.4f" % (self.axisletters[col], qualified))
|
||||
elif row == 1: # current rotational
|
||||
if col == 2: # Z axis only
|
||||
ACTION.CALL_MDI("G10 L2 P0 R %10.4f" % (qualified))
|
||||
elif row == 2: # G92 offset
|
||||
ACTION.CALL_MDI("G92 %s %10.4f" % (self.axisletters[col], qualified))
|
||||
elif row == 3: # Tool
|
||||
if not self.current_tool == 0:
|
||||
ACTION.CALL_MDI("G10 L1 P%d %s %10.4f" % (self.current_tool, self.axisletters[col], qualified))
|
||||
ACTION.CALL_MDI('g43')
|
||||
elif col == 9: # rotational
|
||||
ACTION.CALL_MDI("G10 L2 P%d R %10.4f" % (row-3, qualified))
|
||||
else:
|
||||
ACTION.CALL_MDI("G10 L2 P%d %s %10.4f" % (row-3, self.axisletters[col], qualified))
|
||||
|
||||
ACTION.UPDATE_VAR_FILE()
|
||||
ACTION.RESTORE_RECORDED_MODE()
|
||||
STATUS.emit('reload-display')
|
||||
self.reload_offsets()
|
||||
except Exception as e:
|
||||
LOG.exception("offsetpage widget error: MDI call error", exc_info=e)
|
||||
self.reload_offsets()
|
||||
|
||||
# only update every 10th time periodic calls
|
||||
def periodic_check(self, w):
|
||||
if self.delay < 9:
|
||||
self.delay += 1
|
||||
return
|
||||
else:
|
||||
self.delay = 0
|
||||
if self.filename:
|
||||
self.reload_offsets()
|
||||
return True
|
||||
|
||||
# moves the selection up
|
||||
@Slot()
|
||||
def up(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveUp,Qt.NoModifier))
|
||||
|
||||
# moves the selection down
|
||||
@Slot()
|
||||
def down(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveDown,Qt.NoModifier))
|
||||
|
||||
def left(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveLeft,Qt.NoModifier))
|
||||
|
||||
def right(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveRight,Qt.NoModifier))
|
||||
|
||||
#########################################################################
|
||||
# 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 set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialog_code_string = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_keyboard_code(self, data):
|
||||
self.text_dialog_code = data
|
||||
def get_keyboard_code(self):
|
||||
return self.text_dialog_code
|
||||
def reset_keyboard_code(self):
|
||||
self.text_dialog_code = 'KEYBOARD'
|
||||
text_dialog_code_string = Property(str, get_keyboard_code, set_keyboard_code, reset_keyboard_code)
|
||||
|
||||
def setmetrictemplate(self, data):
|
||||
self.metric_text_template = data
|
||||
def getmetrictemplate(self):
|
||||
return self.metric_text_template
|
||||
def resetmetrictemplate(self):
|
||||
self.metric_text_template = '%10.3f'
|
||||
metric_template = Property(str, getmetrictemplate, setmetrictemplate, resetmetrictemplate)
|
||||
|
||||
def setimperialtexttemplate(self, data):
|
||||
self.imperial_text_template = data
|
||||
def getimperialtexttemplate(self):
|
||||
return self.imperial_text_template
|
||||
def resetimperialtexttemplate(self):
|
||||
self.imperial_text_template = '%9.4f'
|
||||
imperial_template = Property(str, getimperialtexttemplate, setimperialtexttemplate, resetimperialtexttemplate)
|
||||
|
||||
def getColorHighlight(self):
|
||||
return QColor(self.tablemodel._highlightcolor)
|
||||
def setColorHighlight(self, value):
|
||||
self.tablemodel._highlightcolor = value.name()
|
||||
#self.tablemodel.layoutChanged.emit()
|
||||
styleColorHighlight = Property(QColor, getColorHighlight, setColorHighlight)
|
||||
|
||||
#########################################
|
||||
# custom model
|
||||
#########################################
|
||||
class MyTableModel(QAbstractTableModel):
|
||||
def __init__(self, datain, headerdata, vheaderdata, parent=None):
|
||||
"""
|
||||
Args:
|
||||
datain: a list of lists\n
|
||||
headerdata: a list of strings
|
||||
"""
|
||||
super(MyTableModel, self).__init__(parent)
|
||||
self.arraydata = datain
|
||||
self.headerdata = headerdata
|
||||
self.Vheaderdata = vheaderdata
|
||||
self._highlightcolor = '#00ffff'
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.arraydata)
|
||||
|
||||
def columnCount(self, parent):
|
||||
if len(self.arraydata) > 0:
|
||||
return len(self.arraydata[0])
|
||||
return 0
|
||||
|
||||
def data(self, index, role):
|
||||
if role == Qt.EditRole:
|
||||
return self.arraydata[index.row()][index.column()]
|
||||
if role == Qt.DisplayRole:
|
||||
return self.arraydata[index.row()][index.column()]
|
||||
elif role == Qt.BackgroundRole:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
if (isinstance(value, int) or isinstance(value, float) or
|
||||
isinstance(value, str)):
|
||||
if int(index.row()) == self.parent()._system_int + 3:
|
||||
return QColor(self._highlightcolor)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return None
|
||||
# print(">>> flags() index.column() = ", index.column())
|
||||
if index.column() == 10 and index.row() in(0, 1, 2, 3):
|
||||
return Qt.ItemIsEnabled
|
||||
elif index.row() == 0:
|
||||
return Qt.ItemIsEnabled
|
||||
elif index.row() == 1 and not index.column() == 2:
|
||||
return Qt.NoItemFlags
|
||||
elif index.row() in(2, 3) and index.column() == 9:
|
||||
return Qt.NoItemFlags
|
||||
else:
|
||||
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if not index.isValid():
|
||||
return False
|
||||
LOG.debug(self.arraydata[index.row()][index.column()])
|
||||
LOG.debug(">>> setData() role = {}".format(role))
|
||||
LOG.debug(">>> setData() index.column() = {}".format(index.column()))
|
||||
if index.row() == 0: return False
|
||||
try:
|
||||
if index.column() == 10:
|
||||
v = str(value)
|
||||
else:
|
||||
v = float(value)
|
||||
except:
|
||||
return False
|
||||
LOG.debug(">>> setData() value = {}".format(value))
|
||||
LOG.debug(">>> setData() qualified value = {}".format(v))
|
||||
# self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index)
|
||||
LOG.debug(">>> setData() index.row = {}".format(index.row()))
|
||||
LOG.debug(">>> setData() index.column = {}".format(index.column()))
|
||||
self.arraydata[index.row()][index.column()] = v
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
|
||||
def headerData(self, col, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.headerdata[col]
|
||||
if orientation != Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.Vheaderdata[col]
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication([])
|
||||
w = OriginOffsetView()
|
||||
w.PREFS_ = None
|
||||
w._hal_init()
|
||||
w.setProperty('styleColorHighlight',QColor('purple'))
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
435
lib/python/qtvcp/widgets/overlay_widget.py
Normal file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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 os
|
||||
|
||||
from qtpy.QtWidgets import (QWidget, QLabel, QHBoxLayout,
|
||||
QVBoxLayout, QPushButton, QDialog, QProgressBar)
|
||||
from qtpy.QtCore import (Qt, QEvent, Slot, Property, QChildEvent,
|
||||
)
|
||||
from qtpy.QtGui import QColor, QImage, QResizeEvent, QPainter, QMoveEvent
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
if __name__ != '__main__':
|
||||
STATUS = Status()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Set the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
################################################################
|
||||
# Overlay base class
|
||||
# based on c++ code on net
|
||||
# Originally designed to allow an overlay on individual widgets
|
||||
# which might be nice too but for now just the wholw window
|
||||
################################################################
|
||||
class OverlayWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
self.last = None
|
||||
self.top_level = parent
|
||||
super(OverlayWidget, self).__init__(parent)
|
||||
self.parent = parent
|
||||
#self.setAttribute(Qt.WA_NoSystemBackground)
|
||||
#self.setAttribute(Qt.WA_TransparentForMouseEvents)
|
||||
|
||||
self.setWindowFlags( self.windowFlags() |Qt.Tool |
|
||||
Qt.FramelessWindowHint |
|
||||
Qt.WindowStaysOnTopHint )
|
||||
if not self.parent is None:
|
||||
self.parent.installEventFilter(self)
|
||||
|
||||
# There seems to be no way to get the very top level widget
|
||||
# in QT the parent widget can be owned in another parent
|
||||
# this adds an event filter the widget in 'top_level'
|
||||
# so we can track what is happening to it.
|
||||
def newParent(self):
|
||||
self.parent = self.top_level
|
||||
self.last = self.parent
|
||||
if self.last is not None:
|
||||
LOG.debug('last removed: {} NEW:{}'.format(self.last, self.parent))
|
||||
self.last.removeEventFilter(self)
|
||||
if not self.parent: return
|
||||
self.parent.installEventFilter(self)
|
||||
#self.raise_()
|
||||
|
||||
# Here we track parent window events that we need to react to.
|
||||
# then we pass then to the original widget
|
||||
def eventFilter(self, obj, event):
|
||||
# might be useful events to know
|
||||
# 103 window blocked by modal dialog
|
||||
# 99 activation changed
|
||||
# 25 deactivated
|
||||
# 24 activated
|
||||
#print event,'parent',self.parent
|
||||
#print event,'Event Type',self.event.type()
|
||||
if obj == self.parent:
|
||||
#Catches resize and child events from the parent widget
|
||||
if event.type() == QEvent.Resize:
|
||||
#print 'resize'
|
||||
self.resize(QResizeEvent.size(event))
|
||||
elif event.type() == QEvent.Move:
|
||||
self.move(QMoveEvent.pos(event))
|
||||
elif(event.type() == QEvent.ChildAdded):
|
||||
#print 'CHILD',QChildEvent.child(event)
|
||||
if not QChildEvent.child(event) is QDialog:
|
||||
self.raise_()
|
||||
if event.type() == QEvent.Close:
|
||||
self.hide()
|
||||
self.closeEvent(event)
|
||||
event.accept()
|
||||
return super(OverlayWidget, self).eventFilter(obj, event)
|
||||
|
||||
# Tracks our instanance of focus widget changes
|
||||
# most importantly the paint event
|
||||
# if the parent changes it adjusts the event filter
|
||||
# but in our case the parent won't because we are
|
||||
# tracking the main window rather the the widget focus is in
|
||||
def event(self, event):
|
||||
#print 'overlay:',event
|
||||
if event.type() == QEvent.ParentAboutToChange:
|
||||
#print 'REMOVE FILTER'
|
||||
self.parent.removeEventFilter(self)
|
||||
return True
|
||||
if event.type() == QEvent.ParentChange:
|
||||
#print 'parentEVENT:', self.parentWidget()
|
||||
self.newParent()
|
||||
return True
|
||||
if event.type() == QEvent.Paint:
|
||||
self.paintEvent(event)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
####################################################################
|
||||
# subclass for using as a fancy dialog focus overlay
|
||||
####################################################################
|
||||
class FocusOverlay(OverlayWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(FocusOverlay, self).__init__(parent)
|
||||
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.bg_color = QColor(0, 0, 0, 150)
|
||||
self.text = "Loading..."
|
||||
self._state = False
|
||||
self._image_path = '~/emc-dev/linuxcnc-wizard.gif'
|
||||
self._image = QImage(os.path.expanduser(self._image_path))
|
||||
self._show_buttons = False
|
||||
self._show_image = False
|
||||
self._image_transp = 0.3
|
||||
self._show_text = True
|
||||
self.box()
|
||||
|
||||
# This is how we plant the very top level parent into
|
||||
# our overlay widget
|
||||
# QTVCP_INSTANCE is not accessible from Designer
|
||||
# it should work with any main window but doesn't.
|
||||
# this worked so i stopped looking why
|
||||
# this also sets up following show or hide etc based on
|
||||
# STATUS messages
|
||||
# adjust image path name at runtime
|
||||
def _hal_init(self):
|
||||
STATUS.connect('focus-overlay-changed', lambda w, data, text, color:
|
||||
self._status_response(data, text, color))
|
||||
STATUS.connect('graphics-loading-progress',lambda w,f: self.updateProgress(f))
|
||||
if self.PREFS_:
|
||||
self.play_sound = self.PREFS_.getpref('overlay_play_sound', False, bool, 'SCREEN_OPTIONS')
|
||||
self.sound_type = self.PREFS_.getpref('overlay_sound_type', 'RING', str, 'SCREEN_OPTIONS')
|
||||
else:
|
||||
self.play_sound = False
|
||||
|
||||
# reparent on top window
|
||||
self.top_level = self.QTVCP_INSTANCE_
|
||||
self.newParent()
|
||||
|
||||
# adjust size and location to top window
|
||||
self.setGeometry(self.top_level.geometry())
|
||||
self.hide()
|
||||
|
||||
# look for special path names and change to real path
|
||||
if 'STD_IMAGE_DIR/' in self._image_path:
|
||||
t = self._image_path.split('STD_IMAGE_DIR/', )[1]
|
||||
d = os.path.join(self.PATHS_.IMAGEDIR, t)
|
||||
elif 'CONFIG_DIR/' in self._image_path:
|
||||
t = self._image_path.split('CONFIG_DIR/', )[1]
|
||||
d = os.path.join(self.PATHS_.CONFIGPATH, t)
|
||||
else:
|
||||
return
|
||||
if os.path.exists(d):
|
||||
self._image = QImage(d)
|
||||
else:
|
||||
LOG.debug('Focus Overlay image path runtime error: {}'.format(self._image_path))
|
||||
|
||||
def _status_response(self, data, text, color):
|
||||
if data:
|
||||
if color:
|
||||
self.bg_color = color
|
||||
else:
|
||||
self.bg_color = QColor(0, 0, 0, 150)
|
||||
if text:
|
||||
self.setShowText(True)
|
||||
self.text = text
|
||||
else:
|
||||
self.setShowText(False)
|
||||
self.show()
|
||||
self.update()
|
||||
LOG.debug('Overlay - Show')
|
||||
if self.play_sound:
|
||||
STATUS.emit('play-sound', '%s' % self.sound_type)
|
||||
#os.system("beep -f 555 ")
|
||||
else:
|
||||
self.hide()
|
||||
LOG.debug('Overlay - Hide')
|
||||
|
||||
# Ok paint everything
|
||||
def paintEvent(self, event):
|
||||
qp = QPainter()
|
||||
qp.begin(self)
|
||||
self.colorBackground(qp)
|
||||
if self._show_image:
|
||||
self.draw_image(qp)
|
||||
self.drawText(qp)
|
||||
qp.end()
|
||||
|
||||
#################################################
|
||||
# Helper functions
|
||||
#################################################
|
||||
|
||||
def updateProgress(self,fraction):
|
||||
if fraction <0:
|
||||
self.progressbar.setValue(0)
|
||||
self.progressbar.setFormat('')
|
||||
self.progressbar.setVisible(False)
|
||||
self.hide()
|
||||
else:
|
||||
self.progressbar.setVisible(True)
|
||||
self.progressbar.setValue(fraction)
|
||||
self.progressbar.setFormat('Loading: {}%'.format(fraction))
|
||||
|
||||
# The basic overlay -transparence is set by bg_color
|
||||
def colorBackground(self, qp):
|
||||
qp.fillRect(self.rect(), self.bg_color)
|
||||
|
||||
# we can add an image like a watermark transparence set by
|
||||
# _image_transp
|
||||
def draw_image(self, qp):
|
||||
#print self._image, self._image_transp
|
||||
qp.setOpacity(self._image_transp)
|
||||
qp.drawImage(self.rect(), self._image)
|
||||
|
||||
# This sets the text that was built in the box
|
||||
# old code for text is skipped
|
||||
def drawText(self, qp):
|
||||
qp.setOpacity(1.0)
|
||||
self.mb.setText('<html><head/><body><p><span style=" font-size:30pt; \
|
||||
font-weight:600;">%s</span></p></body></html>' % self.text)
|
||||
return
|
||||
size = self.size()
|
||||
w = size.width()
|
||||
h = size.height()
|
||||
qp.setPen(self.text_color)
|
||||
qp.setFont(self.font)
|
||||
qp.drawText(self.rect(), Qt.AlignCenter, self.text)
|
||||
|
||||
# build a black label with text and optionally some buttons
|
||||
# using a label rather then drawing text allows options suck as rich text
|
||||
# and a solid back ground so text is easy to see
|
||||
# buttons could be used as a dialog but would require a focus widget per
|
||||
# dialog action or some fancy coding for responses.
|
||||
def box(self):
|
||||
hbox2 = QHBoxLayout()
|
||||
hbox2.addStretch(1)
|
||||
self.mb = QLabel('<html><head/><body><p><span style=" font-size:30pt; \
|
||||
font-weight:600;">%s</span></p></body></html>' % self.text, self)
|
||||
self.mb.setStyleSheet("background-color: black; color: white")
|
||||
self.mb.setAlignment(Qt.AlignVCenter | Qt.AlignCenter)
|
||||
self.mb.setVisible(self._show_text)
|
||||
hbox2.addWidget(self.mb)
|
||||
|
||||
self.progressbar = QProgressBar()
|
||||
self.progressbar.setStyleSheet("background-color: black; color: white")
|
||||
self.progressbar.setRange(0,100)
|
||||
self.progressbar.setVisible(False)
|
||||
hbox2.addWidget(self.progressbar)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addStretch(1)
|
||||
self.okButton = QPushButton("OK")
|
||||
self.okButton.pressed.connect(self.okChecked)
|
||||
self.cancelButton = QPushButton("Cancel")
|
||||
self.cancelButton.pressed.connect(self.cancelChecked)
|
||||
self.okButton.setVisible(self._show_buttons)
|
||||
self.cancelButton.setVisible(self._show_buttons)
|
||||
l = QLabel()
|
||||
hbox.addWidget(l)
|
||||
hbox.addWidget(self.okButton)
|
||||
hbox.addWidget(self.cancelButton)
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addStretch(1)
|
||||
vbox.addLayout(hbox2)
|
||||
vbox.addLayout(hbox)
|
||||
self.setLayout(vbox)
|
||||
self.setGeometry(300, 300, 300, 150)
|
||||
|
||||
# would need to class patch for something really useful
|
||||
def okChecked(self):
|
||||
self.hide()
|
||||
def cancelChecked(self):
|
||||
self.hide()
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
########################################################################
|
||||
|
||||
@Slot(bool)
|
||||
def setState(self, value):
|
||||
self._state = value
|
||||
if value:
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
def getState(self):
|
||||
return self._state
|
||||
def resetState(self):
|
||||
self._state = False
|
||||
|
||||
def getOverayColor(self):
|
||||
return self.bg_color
|
||||
@Slot(QColor)
|
||||
def setOverayColor(self, value):
|
||||
self.bg_color = value
|
||||
def resetOverayColor(self, value):
|
||||
self.bg_color = value
|
||||
|
||||
def setShowText(self, value):
|
||||
self._show_text = value
|
||||
self.mb.setVisible(value)
|
||||
def getShowText(self):
|
||||
return self._show_text
|
||||
def resetShowText(self, value):
|
||||
self._show_text = True
|
||||
self.mb.setVisible(value)
|
||||
|
||||
def setshow_image(self, data):
|
||||
self._show_image = data
|
||||
#self.hide()
|
||||
#self.show()
|
||||
def getshow_image(self):
|
||||
return self._show_image
|
||||
def resetshow_image(self):
|
||||
self._show_image = False
|
||||
|
||||
def set_image_transp(self, data):
|
||||
self._image_transp = data
|
||||
#self.hide()
|
||||
#self.show()
|
||||
def get_image_transp(self):
|
||||
return self._image_transp
|
||||
def reset_image_transp(self):
|
||||
self._image_transp = 0.3
|
||||
|
||||
def setshow_buttons(self, data):
|
||||
self._show_buttons = data
|
||||
self.okButton.setVisible(data)
|
||||
self.cancelButton.setVisible(data)
|
||||
|
||||
def getshow_buttons(self):
|
||||
return self._show_buttons
|
||||
def resetshow_buttons(self):
|
||||
self._show_buttons = False
|
||||
|
||||
def setimage_path(self, data):
|
||||
self._image_path = str(data)
|
||||
try:
|
||||
path = os.path.expanduser(self._image_path)
|
||||
self._image = QImage(path)
|
||||
except Exception as e:
|
||||
LOG.debug('Focus Overlay image path error: {}'.format(self._image_path))
|
||||
def getimage_path(self):
|
||||
return self._image_path
|
||||
def resetimage_path(self):
|
||||
self._image_path = False
|
||||
|
||||
overlay_color = Property(QColor, getOverayColor, setOverayColor, resetOverayColor)
|
||||
state = Property(bool, getState, setState, resetState)
|
||||
show_text = Property(bool, getShowText, setShowText, resetShowText)
|
||||
show_image_option = Property(bool, getshow_image, setshow_image, resetshow_image)
|
||||
image_transparency = Property(float, get_image_transp, set_image_transp, reset_image_transp)
|
||||
show_buttons_option = Property(bool, getshow_buttons, setshow_buttons, resetshow_buttons)
|
||||
image_path = Property(str, getimage_path, setimage_path, resetimage_path)
|
||||
|
||||
|
||||
#################
|
||||
# Testing
|
||||
#################
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QWidget, QApplication
|
||||
from qtpy.QtCore import QTimer
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
w = QWidget()
|
||||
#w.setWindowFlags( Qt.FramelessWindowHint | Qt.Dialog | Qt.WindowStaysOnTopHint )
|
||||
w.setGeometry(300, 300, 250, 150)
|
||||
w.setWindowTitle('Test')
|
||||
|
||||
l = QLabel('Hello, world!', w)
|
||||
l.show()
|
||||
LOG.debug('Label: {}'.format(l))
|
||||
|
||||
# class patching is my new thing
|
||||
# class patch the OK button
|
||||
def newOk(w):
|
||||
print('Ok')
|
||||
w.text = 'OK'
|
||||
# make update
|
||||
w.hide()
|
||||
w.show()
|
||||
|
||||
def pop():
|
||||
print('Pop')
|
||||
f.show()
|
||||
f.update()
|
||||
|
||||
FocusOverlay.okChecked = newOk
|
||||
w.show()
|
||||
# could use f = FocusOverlay( w )
|
||||
# then dont need to adjust top level
|
||||
f = FocusOverlay()
|
||||
f.top_level = w
|
||||
f.newParent()
|
||||
# set with qtproperty call
|
||||
f.setshow_buttons(True)
|
||||
|
||||
|
||||
|
||||
timer2 = QTimer()
|
||||
timer2.timeout.connect(lambda:pop() )
|
||||
timer2.start(1500)
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2054
lib/python/qtvcp/widgets/probe_routines.py
Normal file
280
lib/python/qtvcp/widgets/probe_subprog.py
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp probe subprogram
|
||||
#
|
||||
# Copyright (c) 2018 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
# This subprogram is used by both versa_probe and basic_probe widgets
|
||||
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp.widgets.probe_routines import ProbeRoutines
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
class ProbeSubprog(QObject, ProbeRoutines):
|
||||
def __init__(self):
|
||||
QObject.__init__(self)
|
||||
ProbeRoutines.__init__(self)
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
self._format_template = "%3.3f"
|
||||
else:
|
||||
self._format_template = "%2.4f"
|
||||
|
||||
self.send_dict = {}
|
||||
self.error_status = None
|
||||
# list of parameters received from main probe program
|
||||
# excluding booleans, these are handled separately
|
||||
self.parm_list = ['probe_diam',
|
||||
'latch_return_dist',
|
||||
'max_travel',
|
||||
'max_z_travel',
|
||||
'search_vel',
|
||||
'probe_vel',
|
||||
'rapid_vel',
|
||||
'side_edge_length',
|
||||
'xy_clearance',
|
||||
'adj_x',
|
||||
'adj_y',
|
||||
'adj_z',
|
||||
'adj_angle',
|
||||
'x_hint_bp',
|
||||
'y_hint_bp',
|
||||
'x_hint_rv',
|
||||
'y_hint_rv',
|
||||
'diameter_hint',
|
||||
'z_clearance',
|
||||
'extra_depth',
|
||||
'cal_x_width',
|
||||
'cal_y_width',
|
||||
'cal_diameter',
|
||||
'calibration_offset',
|
||||
'ts_diam',
|
||||
'z_max_clear',
|
||||
'ts_x',
|
||||
'ts_y',
|
||||
'ts_z',
|
||||
'ts_max',
|
||||
'tool_diameter',
|
||||
'tool_number',
|
||||
'tool_probe_height',
|
||||
'tool_block_height']
|
||||
|
||||
# data structure to hold parameters
|
||||
# common
|
||||
self.data_probe_diam = 1.0
|
||||
self.data_latch_return_dist = 1.0
|
||||
self.data_search_vel = 10.0
|
||||
self.data_probe_vel = 10.0
|
||||
self.data_rapid_vel = 10.0
|
||||
self.data_max_travel = 10.0
|
||||
self.data_max_z_travel = 10.0
|
||||
self.data_side_edge_length = 1.0
|
||||
self.data_xy_clearance = 1.0
|
||||
self.data_z_clearance = 1.0
|
||||
self.data_extra_depth = 0.0
|
||||
self.allow_auto_zero = False
|
||||
self.allow_auto_skew = False
|
||||
# VersaProbe exclusive
|
||||
self.data_adj_x = 0.0
|
||||
self.data_adj_y = 0.0
|
||||
self.data_adj_z = 0.0
|
||||
self.data_adj_angle = 0.0
|
||||
self.data_ts_diam = None
|
||||
self.data_z_max_clear = None
|
||||
self.data_ts_x= None
|
||||
self.data_ts_y= None
|
||||
self.data_ts_z= None
|
||||
self.data_ts_max = None
|
||||
self.data_tool_diameter = None
|
||||
self.data_tool_number = None
|
||||
self.data_tool_probe_height = None
|
||||
self.data_tool_block_height = None
|
||||
# BasicProbe exclusive
|
||||
self.data_x_hint_bp = 0.0
|
||||
self.data_y_hint_bp = 0.0
|
||||
self.data_x_hint_rv = 0.0
|
||||
self.data_y_hint_rv = 0.0
|
||||
self.data_diameter_hint = 0.0
|
||||
self.data_cal_x_width = 0.0
|
||||
self.data_cal_y_width = 0.0
|
||||
self.data_cal_diameter = 0.0
|
||||
self.data_calibration_offset = 0.0
|
||||
self.cal_avg_error = False
|
||||
self.cal_x_error = False
|
||||
self.cal_y_error = False
|
||||
# list of results to be transferred to main program
|
||||
self.status_list = ['xm', 'xc', 'xp', 'ym', 'yc', 'yp', 'lx', 'ly', 'z', 'd', 'a', 'delta','th','bh']
|
||||
# data structure to hold result values
|
||||
self.status_xm = None
|
||||
self.status_xc = None
|
||||
self.status_xp = None
|
||||
self.status_ym = None
|
||||
self.status_yc = None
|
||||
self.status_yp = None
|
||||
self.status_lx = None
|
||||
self.status_ly = None
|
||||
self.status_z = None
|
||||
self.status_d = None
|
||||
self.status_a = None
|
||||
self.status_delta = None
|
||||
self.status_th = None
|
||||
self.status_bh = None
|
||||
self.history_log = ""
|
||||
|
||||
self.process()
|
||||
|
||||
def process(self):
|
||||
while 1:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
if line:
|
||||
cmd = line
|
||||
line = None
|
||||
try:
|
||||
error = self.process_command(cmd)
|
||||
|
||||
# block polling here 0 main program should start polling in their end
|
||||
STATUS.block_error_polling()
|
||||
|
||||
# error = 1 means success,
|
||||
# error = None means ignore,
|
||||
# anything else is an error - a returned string is an error message
|
||||
if error is not None:
|
||||
if error != 1:
|
||||
if type(error) == str:
|
||||
sys.stdout.write("ERROR INFO Probe routine: {}\n".format(error))
|
||||
else:
|
||||
sys.stdout.write("ERROR Probe routine returned with error\n")
|
||||
else:
|
||||
self.collect_status()
|
||||
sys.stdout.write("COMPLETE$" + json.dumps(self.send_dict) + "\n")
|
||||
sys.stdout.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
# print history
|
||||
if self.history_log != "":
|
||||
time.sleep(0.1)
|
||||
sys.stdout.write("HISTORY {}\n".format(self.history_log))
|
||||
self.history_log = ""
|
||||
sys.stdout.flush()
|
||||
|
||||
except Exception as e:
|
||||
sys.stdout.write("ERROR Command Error: {}\n".format(e))
|
||||
sys.stdout.flush()
|
||||
break
|
||||
|
||||
# check that the command is actually a method in our class else
|
||||
# this message isn't for us - ignore it
|
||||
def process_command(self, cmd):
|
||||
cmd = cmd.rstrip().split('$')
|
||||
if cmd[0] in dir(self):
|
||||
if not STATUS.is_on_and_idle(): return None
|
||||
pre = self.prechecks()
|
||||
if pre is not None: return pre
|
||||
parms = json.loads(cmd[1])
|
||||
self.update_data(parms)
|
||||
# start polling errors here - parent program should have blocked their polling
|
||||
STATUS.unblock_error_polling()
|
||||
error = self[cmd[0]]()
|
||||
if (error != 1 or type(error)== str) and STATUS.is_on_and_idle():
|
||||
ACTION.CALL_MDI("G90")
|
||||
self.postreset()
|
||||
return error
|
||||
else:
|
||||
return 'Command function {} not in probe routines'.format(cmd[0])
|
||||
|
||||
def update_data(self, parms):
|
||||
for key in parms:
|
||||
if key in self.parm_list:
|
||||
try:
|
||||
self['data_' + key] = float(parms[key].encode('utf-8'))
|
||||
except:
|
||||
pass
|
||||
self.allow_auto_zero = True if parms['allow_auto_zero'] == '1' else False
|
||||
self.allow_auto_skew = True if parms['allow_auto_skew'] == '1' else False
|
||||
try:
|
||||
self.cal_avg_error = True if parms['cal_avg_error'] == '1' else False
|
||||
self.cal_x_error = True if parms['cal_x_error'] == '1' else False
|
||||
self.cal_y_error = True if parms['cal_y_error'] == '1' else False
|
||||
except:
|
||||
pass
|
||||
# adjust z_clearance data
|
||||
#self.data_z_clearance += self.data_extra_depth
|
||||
# clear all previous probe results
|
||||
for i in (self.status_list):
|
||||
self['status_' + i] = None
|
||||
|
||||
def collect_status(self):
|
||||
try:
|
||||
tmpl = lambda s: self._format_template % s
|
||||
for key in self.status_list:
|
||||
if self['status_' + key] is None:
|
||||
data = 'None'
|
||||
else:
|
||||
data = tmpl(self['status_' + key])
|
||||
self.send_dict.update( {key: data} )
|
||||
except Exception as e:
|
||||
print('ERROR ',e)
|
||||
return
|
||||
|
||||
#for key in self.status_list:
|
||||
# data = "{:3.3f}".format(self['status_' + key])
|
||||
# self.send_dict.update( {key: data} )
|
||||
#return
|
||||
|
||||
# need to be in the right mode - entries are in machine units
|
||||
def prechecks(self):
|
||||
# This is a work around. If a user sets the spindle running in MDI
|
||||
# but turn it off with a manual button, then when M72 will turn the
|
||||
# spindle back on! So we explicitly set M5 here.
|
||||
ACTION.CALL_MDI('M5')
|
||||
# record current G,S,M codes
|
||||
ACTION.CALL_MDI('M70')
|
||||
|
||||
# set proper mode based on what machine is based
|
||||
if INFO.MACHINE_IS_METRIC and STATUS.is_metric_mode():
|
||||
return None
|
||||
if not INFO.MACHINE_IS_METRIC and not STATUS.is_metric_mode():
|
||||
return None
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
ACTION.CALL_MDI('g21')
|
||||
else:
|
||||
ACTION.CALL_MDI('g20')
|
||||
return None
|
||||
|
||||
# return to previous motion modes
|
||||
def postreset(self):
|
||||
ACTION.CALL_MDI('M72')
|
||||
|
||||
########################################
|
||||
# required boiler code
|
||||
########################################
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
####################################
|
||||
# Testing
|
||||
####################################
|
||||
if __name__ == "__main__":
|
||||
w = ProbeSubprog()
|
||||
|
||||
25
lib/python/qtvcp/widgets/qtvcp_icons.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
# Set up logging
|
||||
from qtvcp import logger
|
||||
log = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
log.setLevel(logger.ERROR) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class Icon:
|
||||
def __init__(self):
|
||||
self.ICONDIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'widget_icons')
|
||||
self.LCNC_ICON = os.path.join(self.ICONDIR, 'linuxcnc-wizard.gif')
|
||||
if not os.path.isfile(self.LCNC_ICON):
|
||||
log.warning('error no icon in: {}'.format(self.LCNC_ICON))
|
||||
|
||||
def get_path(self, widgetname):
|
||||
for i in ('.png', '.gif', 'error'):
|
||||
path = os.path.join(self.ICONDIR, widgetname + i)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
elif i == 'error':
|
||||
log.debug("Missing icon for '{}' widget".format(widgetname))
|
||||
return self.LCNC_ICON
|
||||
79
lib/python/qtvcp/widgets/radio_axis_selector.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
from qtpy import QtCore, QtWidgets
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# ACTION is for linuxcnc commands
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class RadioAxisSelector(QtWidgets.QRadioButton, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(RadioAxisSelector, self).__init__(parent)
|
||||
self.toggled.connect(lambda:self.btnstate(self))
|
||||
self.axis = ''
|
||||
self.joint = -1
|
||||
|
||||
def _hal_init(self):
|
||||
STATUS.connect('motion-mode-changed', lambda w,data: self.modeChanged(data))
|
||||
|
||||
def btnstate(self,b):
|
||||
if b.isChecked() == True:
|
||||
if STATUS.is_joint_mode():
|
||||
ACTION.SET_SELECTED_JOINT(self.joint)
|
||||
else:
|
||||
ACTION.SET_SELECTED_AXIS(self.axis)
|
||||
# set this without causing a STATUS message output
|
||||
# in case we are selecting an axis to un/home
|
||||
STATUS.selected_joint = self.joint
|
||||
|
||||
# change of joints mode / axis mode
|
||||
def modeChanged(self, mode):
|
||||
self.btnstate(self)
|
||||
|
||||
def setAxis(self, data):
|
||||
if data.upper() in('X','Y','Z','A','B','C','U','V','W'):
|
||||
self.axis = str(data.upper())
|
||||
def getAxis(self):
|
||||
return self.axis
|
||||
def resetAxis(self):
|
||||
self.axis = ''
|
||||
|
||||
def setJoint(self, data):
|
||||
self.joint = data
|
||||
def getJoint(self):
|
||||
return self.joint
|
||||
def resetJoint(self):
|
||||
self.joint = -1
|
||||
|
||||
axis_selection = QtCore.Property(str, getAxis, setAxis, resetAxis)
|
||||
joint_selection = QtCore.Property(int, getJoint, setJoint, resetJoint)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
|
||||
cb = RadioAxisSelector()
|
||||
layout.addWidget(cb)
|
||||
cb2 = RadioAxisSelector()
|
||||
layout.addWidget(cb2)
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
410
lib/python/qtvcp/widgets/richtext_selector.py
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
from qtpy.QtGui import *
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtPrintSupport import *
|
||||
|
||||
from qtvcp.core import Info, Path
|
||||
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
PATH = Path()
|
||||
|
||||
FONT_SIZES = [7, 8, 9, 10, 11, 12, 13, 14, 18, 24, 36, 48, 64, 72, 96, 144, 288]
|
||||
IMAGE_EXTENSIONS = ['.jpg','.png','.bmp']
|
||||
HTML_EXTENSIONS = ['.htm', '.html']
|
||||
|
||||
def hexuuid():
|
||||
return uuid.uuid4().hex
|
||||
|
||||
def splitext(p):
|
||||
return os.path.splitext(p)[1].lower()
|
||||
|
||||
class TextEdit(QTextEdit):
|
||||
|
||||
def canInsertFromMimeData(self, source):
|
||||
|
||||
if source.hasImage():
|
||||
return True
|
||||
else:
|
||||
return super(TextEdit, self).canInsertFromMimeData(source)
|
||||
|
||||
def insertFromMimeData(self, source):
|
||||
|
||||
cursor = self.textCursor()
|
||||
document = self.document()
|
||||
|
||||
if source.hasUrls():
|
||||
|
||||
for u in source.urls():
|
||||
file_ext = splitext(str(u.toLocalFile()))
|
||||
if u.isLocalFile() and file_ext in IMAGE_EXTENSIONS:
|
||||
image = QImage(u.toLocalFile())
|
||||
document.addResource(QTextDocument.ImageResource, u, image)
|
||||
cursor.insertImage(u.toLocalFile())
|
||||
|
||||
else:
|
||||
# If we hit a non-image or non-local URL break the loop and fall out
|
||||
# to the super call & let Qt handle it
|
||||
break
|
||||
|
||||
else:
|
||||
# If all were valid images, finish here.
|
||||
return
|
||||
|
||||
|
||||
elif source.hasImage():
|
||||
image = source.imageData()
|
||||
uuid = hexuuid()
|
||||
document.addResource(QTextDocument.ImageResource, uuid, image)
|
||||
cursor.insertImage(uuid)
|
||||
return
|
||||
|
||||
super(TextEdit, self).insertFromMimeData(source)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MainWindow, self).__init__(*args, **kwargs)
|
||||
|
||||
filepath = os.path.join(PATH.SHAREDIR, 'images/widgets/RichTextEditor')
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.editor = TextEdit()
|
||||
|
||||
# Setup the QTextEdit editor configuration
|
||||
self.editor.setAutoFormatting(QTextEdit.AutoAll)
|
||||
self.editor.selectionChanged.connect(self.update_format)
|
||||
# Initialize default font size.
|
||||
font = QFont('Times', 12)
|
||||
self.editor.setFont(font)
|
||||
# We need to repeat the size to init the current format.
|
||||
self.editor.setFontPointSize(12)
|
||||
|
||||
# self.path holds the path of the currently open file.
|
||||
# If none, we haven't got a file open yet (or creating new).
|
||||
self.path = None
|
||||
|
||||
layout.addWidget(self.editor)
|
||||
|
||||
container = QWidget()
|
||||
container.setLayout(layout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
self.status = QStatusBar()
|
||||
self.setStatusBar(self.status)
|
||||
|
||||
# Uncomment to disable native menubar on Mac
|
||||
# self.menuBar().setNativeMenuBar(False)
|
||||
|
||||
|
||||
|
||||
edit_toolbar = QToolBar("Edit")
|
||||
edit_toolbar.setIconSize(QSize(16, 16))
|
||||
self.addToolBar(edit_toolbar)
|
||||
edit_menu = self.menuBar().addMenu("&Edit")
|
||||
|
||||
undo_action = QAction(QIcon(os.path.join(filepath, 'arrow-curve-180-left.png')), "Undo", self)
|
||||
undo_action.setStatusTip("Undo last change")
|
||||
undo_action.triggered.connect(self.editor.undo)
|
||||
edit_menu.addAction(undo_action)
|
||||
|
||||
redo_action = QAction(QIcon(os.path.join(filepath, 'arrow-curve.png')), "Redo", self)
|
||||
redo_action.setStatusTip("Redo last change")
|
||||
redo_action.triggered.connect(self.editor.redo)
|
||||
edit_toolbar.addAction(redo_action)
|
||||
edit_menu.addAction(redo_action)
|
||||
|
||||
edit_menu.addSeparator()
|
||||
|
||||
cut_action = QAction(QIcon(os.path.join(filepath, 'scissors.png')), "Cut", self)
|
||||
cut_action.setStatusTip("Cut selected text")
|
||||
cut_action.setShortcut(QKeySequence.Cut)
|
||||
cut_action.triggered.connect(self.editor.cut)
|
||||
edit_toolbar.addAction(cut_action)
|
||||
edit_menu.addAction(cut_action)
|
||||
|
||||
copy_action = QAction(QIcon(os.path.join(filepath, 'document-copy.png')), "Copy", self)
|
||||
copy_action.setStatusTip("Copy selected text")
|
||||
cut_action.setShortcut(QKeySequence.Copy)
|
||||
copy_action.triggered.connect(self.editor.copy)
|
||||
edit_toolbar.addAction(copy_action)
|
||||
edit_menu.addAction(copy_action)
|
||||
|
||||
paste_action = QAction(QIcon(os.path.join(filepath, 'clipboard-paste-document-text.png')), "Paste", self)
|
||||
paste_action.setStatusTip("Paste from clipboard")
|
||||
cut_action.setShortcut(QKeySequence.Paste)
|
||||
paste_action.triggered.connect(self.editor.paste)
|
||||
edit_toolbar.addAction(paste_action)
|
||||
edit_menu.addAction(paste_action)
|
||||
|
||||
select_action = QAction(QIcon(os.path.join(filepath, 'selection-input.png')), "Select all", self)
|
||||
select_action.setStatusTip("Select all text")
|
||||
cut_action.setShortcut(QKeySequence.SelectAll)
|
||||
select_action.triggered.connect(self.editor.selectAll)
|
||||
edit_menu.addAction(select_action)
|
||||
|
||||
edit_menu.addSeparator()
|
||||
|
||||
wrap_action = QAction(QIcon(os.path.join(filepath, 'arrow-continue.png')), "Wrap text to window", self)
|
||||
wrap_action.setStatusTip("Toggle wrap text to window")
|
||||
wrap_action.setCheckable(True)
|
||||
wrap_action.setChecked(True)
|
||||
wrap_action.triggered.connect(self.edit_toggle_wrap)
|
||||
edit_menu.addAction(wrap_action)
|
||||
|
||||
format_toolbar = QToolBar("Format")
|
||||
format_toolbar.setIconSize(QSize(16, 16))
|
||||
self.addToolBar(format_toolbar)
|
||||
format_menu = self.menuBar().addMenu("&Format")
|
||||
|
||||
# We need references to these actions/settings to update as selection changes, so attach to self.
|
||||
self.fonts = QFontComboBox()
|
||||
self.fonts.currentFontChanged.connect(self.editor.setCurrentFont)
|
||||
format_toolbar.addWidget(self.fonts)
|
||||
|
||||
self.fontsize = QComboBox()
|
||||
self.fontsize.addItems([str(s) for s in FONT_SIZES])
|
||||
|
||||
# Connect to the signal producing the text of the current selection. Convert the string to float
|
||||
# and set as the pointsize. We could also use the index + retrieve from FONT_SIZES.
|
||||
self.fontsize.currentIndexChanged[str].connect(lambda s: self.editor.setFontPointSize(float(s)) )
|
||||
format_toolbar.addWidget(self.fontsize)
|
||||
|
||||
self.bold_action = QAction(QIcon(os.path.join(filepath, 'edit-bold.png')), "Bold", self)
|
||||
self.bold_action.setStatusTip("Bold")
|
||||
self.bold_action.setShortcut(QKeySequence.Bold)
|
||||
self.bold_action.setCheckable(True)
|
||||
self.bold_action.toggled.connect(lambda x: self.editor.setFontWeight(QFont.Bold if x else QFont.Normal))
|
||||
format_toolbar.addAction(self.bold_action)
|
||||
format_menu.addAction(self.bold_action)
|
||||
|
||||
self.color_button = QPushButton('Color')
|
||||
self.color_button.clicked.connect(self.update_color)
|
||||
wac = QWidgetAction(format_toolbar)
|
||||
wac.setDefaultWidget(self.color_button)
|
||||
format_toolbar.addAction(wac)
|
||||
#format_menu.addAction(self.color_action)
|
||||
|
||||
self.italic_action = QAction(QIcon(os.path.join(filepath, 'edit-italic.png')), "Italic", self)
|
||||
self.italic_action.setStatusTip("Italic")
|
||||
self.italic_action.setShortcut(QKeySequence.Italic)
|
||||
self.italic_action.setCheckable(True)
|
||||
self.italic_action.toggled.connect(self.editor.setFontItalic)
|
||||
format_toolbar.addAction(self.italic_action)
|
||||
format_menu.addAction(self.italic_action)
|
||||
|
||||
self.underline_action = QAction(QIcon(os.path.join(filepath, 'edit-underline.png')), "Underline", self)
|
||||
self.underline_action.setStatusTip("Underline")
|
||||
self.underline_action.setShortcut(QKeySequence.Underline)
|
||||
self.underline_action.setCheckable(True)
|
||||
self.underline_action.toggled.connect(self.editor.setFontUnderline)
|
||||
format_toolbar.addAction(self.underline_action)
|
||||
format_menu.addAction(self.underline_action)
|
||||
|
||||
format_menu.addSeparator()
|
||||
|
||||
self.alignl_action = QAction(QIcon(os.path.join(filepath, 'edit-alignment.png')), "Align left", self)
|
||||
self.alignl_action.setStatusTip("Align text left")
|
||||
self.alignl_action.setCheckable(True)
|
||||
self.alignl_action.triggered.connect(lambda: self.editor.setAlignment(Qt.AlignLeft))
|
||||
format_toolbar.addAction(self.alignl_action)
|
||||
format_menu.addAction(self.alignl_action)
|
||||
|
||||
self.alignc_action = QAction(QIcon(os.path.join(filepath, 'edit-alignment-center.png')), "Align center", self)
|
||||
self.alignc_action.setStatusTip("Align text center")
|
||||
self.alignc_action.setCheckable(True)
|
||||
self.alignc_action.triggered.connect(lambda: self.editor.setAlignment(Qt.AlignCenter))
|
||||
format_toolbar.addAction(self.alignc_action)
|
||||
format_menu.addAction(self.alignc_action)
|
||||
|
||||
self.alignr_action = QAction(QIcon(os.path.join(filepath, 'edit-alignment-right.png')), "Align right", self)
|
||||
self.alignr_action.setStatusTip("Align text right")
|
||||
self.alignr_action.setCheckable(True)
|
||||
self.alignr_action.triggered.connect(lambda: self.editor.setAlignment(Qt.AlignRight))
|
||||
format_toolbar.addAction(self.alignr_action)
|
||||
format_menu.addAction(self.alignr_action)
|
||||
|
||||
self.alignj_action = QAction(QIcon(os.path.join(filepath, 'edit-alignment-justify.png')), "Justify", self)
|
||||
self.alignj_action.setStatusTip("Justify text")
|
||||
self.alignj_action.setCheckable(True)
|
||||
self.alignj_action.triggered.connect(lambda: self.editor.setAlignment(Qt.AlignJustify))
|
||||
format_toolbar.addAction(self.alignj_action)
|
||||
format_menu.addAction(self.alignj_action)
|
||||
|
||||
format_group = QActionGroup(self)
|
||||
format_group.setExclusive(True)
|
||||
format_group.addAction(self.alignl_action)
|
||||
format_group.addAction(self.alignc_action)
|
||||
format_group.addAction(self.alignr_action)
|
||||
format_group.addAction(self.alignj_action)
|
||||
|
||||
format_menu.addSeparator()
|
||||
|
||||
# A list of all format-related widgets/actions, so we can disable/enable signals when updating.
|
||||
self._format_actions = [
|
||||
self.fonts,
|
||||
self.fontsize,
|
||||
self.bold_action,
|
||||
self.italic_action,
|
||||
self.underline_action,
|
||||
# We don't need to disable signals for alignment, as they are paragraph-wide.
|
||||
]
|
||||
|
||||
# Initialize.
|
||||
self.update_format()
|
||||
self.update_title()
|
||||
self.color_button.setStyleSheet("background-color: {}".format(self.editor.textColor().name()))
|
||||
self.show()
|
||||
|
||||
def block_signals(self, objects, b):
|
||||
for o in objects:
|
||||
o.blockSignals(b)
|
||||
|
||||
def update_format(self):
|
||||
"""
|
||||
Update the font format toolbar/actions when a new text selection is made. This is necessary to keep
|
||||
toolbars/etc. in sync with the current edit state.
|
||||
:return:
|
||||
"""
|
||||
# Disable signals for all format widgets, so changing values here does not trigger further formatting.
|
||||
self.block_signals(self._format_actions, True)
|
||||
|
||||
self.fonts.setCurrentFont(self.editor.currentFont())
|
||||
# Nasty, but we get the font-size as a float but want it was an int
|
||||
self.fontsize.setCurrentText(str(int(self.editor.fontPointSize())))
|
||||
|
||||
self.italic_action.setChecked(self.editor.fontItalic())
|
||||
self.underline_action.setChecked(self.editor.fontUnderline())
|
||||
self.bold_action.setChecked(self.editor.fontWeight() == QFont.Bold)
|
||||
|
||||
self.alignl_action.setChecked(self.editor.alignment() == Qt.AlignLeft)
|
||||
self.alignc_action.setChecked(self.editor.alignment() == Qt.AlignCenter)
|
||||
self.alignr_action.setChecked(self.editor.alignment() == Qt.AlignRight)
|
||||
self.alignj_action.setChecked(self.editor.alignment() == Qt.AlignJustify)
|
||||
|
||||
self.block_signals(self._format_actions, False)
|
||||
|
||||
def dialog_critical(self, s):
|
||||
dlg = QMessageBox(self)
|
||||
dlg.setText(s)
|
||||
dlg.setIcon(QMessageBox.Critical)
|
||||
dlg.show()
|
||||
|
||||
def file_open(self):
|
||||
path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "HTML documents (*.html);Text documents (*.txt);All files (*.*)")
|
||||
|
||||
try:
|
||||
with open(path, 'rU') as f:
|
||||
text = f.read()
|
||||
|
||||
except Exception as e:
|
||||
self.dialog_critical(str(e))
|
||||
|
||||
else:
|
||||
self.path = path
|
||||
# Qt will automatically try and guess the format as txt/html
|
||||
self.editor.setText(text)
|
||||
self.update_title()
|
||||
|
||||
def file_save(self):
|
||||
if self.path is None:
|
||||
# If we do not have a path, we need to use Save As.
|
||||
return self.file_saveas()
|
||||
|
||||
text = self.editor.toHtml() if splitext(self.path) in HTML_EXTENSIONS else self.editor.toPlainText()
|
||||
|
||||
try:
|
||||
with open(self.path, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
except Exception as e:
|
||||
self.dialog_critical(str(e))
|
||||
|
||||
def file_saveas(self):
|
||||
path, _ = QFileDialog.getSaveFileName(self, "Save file", "", "HTML documents (*.html);Text documents (*.txt);All files (*.*)")
|
||||
|
||||
if not path:
|
||||
# If dialog is cancelled, will return ''
|
||||
return
|
||||
|
||||
text = self.editor.toHtml() if splitext(path) in HTML_EXTENSIONS else self.editor.toPlainText()
|
||||
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
f.write(text)
|
||||
|
||||
except Exception as e:
|
||||
self.dialog_critical(str(e))
|
||||
|
||||
else:
|
||||
self.path = path
|
||||
self.update_title()
|
||||
|
||||
def file_print(self):
|
||||
print(self.editor.toHtml())
|
||||
|
||||
def update_title(self):
|
||||
self.setWindowTitle("%s - Megasolid Idiom" % (os.path.basename(self.path) if self.path else "Untitled"))
|
||||
|
||||
def edit_toggle_wrap(self):
|
||||
self.editor.setLineWrapMode( 1 if self.editor.lineWrapMode() == 0 else 0 )
|
||||
|
||||
def update_color(self):
|
||||
color = QColorDialog.getColor()
|
||||
if QColor.isValid(color):
|
||||
self.editor.setTextColor(color)
|
||||
self.color_button.setStyleSheet("background-color: {}".format(color.name()))
|
||||
|
||||
class RichTextEditorDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(RichTextEditorDialog, self).__init__(parent)
|
||||
self.window = MainWindow()
|
||||
l = QVBoxLayout()
|
||||
l.addWidget(self.window)
|
||||
|
||||
self.bBox = QDialogButtonBox()
|
||||
self.bBox.addButton('Apply', QDialogButtonBox.AcceptRole)
|
||||
self.bBox.addButton('Cancel', QDialogButtonBox.RejectRole)
|
||||
self.bBox.rejected.connect(self.reject)
|
||||
self.bBox.accepted.connect(self.accept)
|
||||
l.addWidget(self.bBox)
|
||||
self.setLayout(l)
|
||||
|
||||
def showDialog(self, pretext = None):
|
||||
if pretext:
|
||||
self.window.editor.setText(pretext)
|
||||
retval = self.exec()
|
||||
self.hide()
|
||||
if retval:
|
||||
return self.window.editor.toHtml()
|
||||
else:
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def p():
|
||||
print(window.editor.toHtml())
|
||||
app.exit()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Megasolid Idiom")
|
||||
window = MainWindow()
|
||||
|
||||
d = QDialog()
|
||||
l = QVBoxLayout()
|
||||
l.addWidget(window)
|
||||
|
||||
bBox = QDialogButtonBox()
|
||||
bBox.addButton('Apply', QDialogButtonBox.AcceptRole)
|
||||
bBox.addButton('Cancel', QDialogButtonBox.RejectRole)
|
||||
bBox.rejected.connect(app.exit)
|
||||
bBox.accepted.connect(p)
|
||||
l.addWidget(bBox)
|
||||
|
||||
d.setLayout(l)
|
||||
d.show()
|
||||
app.exec()
|
||||
437
lib/python/qtvcp/widgets/round_gauge.py
Normal file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import math
|
||||
from qtpy import QtCore, QtWidgets
|
||||
from qtpy.QtCore import Qt, QPoint, QPointF, QLine, QRect, QSize, Slot, Property
|
||||
from qtpy.QtGui import QPainter, QBrush, QPen, QFont, QColor, QRadialGradient
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase, hal
|
||||
|
||||
class Gauge(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(Gauge, self).__init__(parent)
|
||||
self._threshold = 0
|
||||
self._setpoint = QPointF(0, 0)
|
||||
self._num_ticks = 11
|
||||
self._max_value = 100
|
||||
self._max_reading = 100
|
||||
self._gauge_label = "GAUGE"
|
||||
self._dial_font_size = 10
|
||||
self._value_font_size = 10
|
||||
self._label_font_size = 10
|
||||
self._base_gradient_color = QColor(180, 180, 180)
|
||||
self._base_color = QColor(40, 40, 40)
|
||||
self._center_gradient_color = QColor("gray")
|
||||
self._center_color = QColor("#404040")
|
||||
self._zone1_color = QColor("green")
|
||||
self._zone2_color = QColor("red")
|
||||
self._bezel_color = QColor("gray")
|
||||
self._bezel_width = 6
|
||||
self.qpa = list()
|
||||
self.canvas = QRect()
|
||||
self.canvas.setSize(QSize(self.width(), self.height()))
|
||||
self.tick_width = 2
|
||||
self.arc_width = 10
|
||||
self.value = 0
|
||||
self._halpin_option = True
|
||||
self.create_unit_array()
|
||||
|
||||
def _hal_init(self):
|
||||
if (self._halpin_option):
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(self.HAL_NAME_ + "_value", hal.HAL_FLOAT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda value: self.update_value(value))
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(self.HAL_NAME_ + "_setpoint", hal.HAL_FLOAT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda value: self.set_setpoint(value))
|
||||
|
||||
def create_unit_array(self):
|
||||
self.qpa = list(range(self._num_ticks))
|
||||
inc = 270.0 / (self._num_ticks - 1)
|
||||
for i in range(self._num_ticks):
|
||||
angle = (inc * i) - 225
|
||||
x = math.cos(math.radians(angle))
|
||||
y = math.sin(math.radians(angle))
|
||||
self.qpa[i] = QPointF(x, y)
|
||||
|
||||
def paintEvent(self, event):
|
||||
w = int(min(event.rect().width(), event.rect().height()))
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(painter.Antialiasing)
|
||||
self.draw_background(painter, event, w)
|
||||
self.draw_zones(painter, event, w)
|
||||
self.draw_gauge(painter, event, w)
|
||||
self.draw_digits(painter, event, w)
|
||||
self.draw_setpoint(painter, event, w)
|
||||
self.draw_needle(painter, event, w)
|
||||
self.draw_center(painter, event, w)
|
||||
self.draw_readout(painter, event, w)
|
||||
painter.end()
|
||||
|
||||
def draw_background(self, qp, event, w):
|
||||
w -= 6
|
||||
center = event.rect().center()
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(w, w))
|
||||
rect.moveCenter(center)
|
||||
fp = QPointF(center.x() - w/4, center.y() - w/4)
|
||||
bg = QRadialGradient(QPointF(center), w/2, fp)
|
||||
bg.setColorAt(0, self._base_gradient_color)
|
||||
bg.setColorAt(1, self._base_color)
|
||||
qp.setPen(QPen(self._bezel_color, self._bezel_width))
|
||||
qp.setBrush(QBrush(bg))
|
||||
qp.drawEllipse(rect)
|
||||
qp.drawArc(rect, 0, 360 * 16)
|
||||
|
||||
def draw_zones(self, qp, event, w):
|
||||
segment1 = -45
|
||||
span1 = int(((self._max_value - self._threshold) * 270) / self._max_value)
|
||||
segment2 = span1 - 45
|
||||
span2 = 270 - span1
|
||||
rect = QRect()
|
||||
w = int(w/2)
|
||||
rect.setSize(QSize(w, w))
|
||||
rect.moveCenter(event.rect().center())
|
||||
qp.setPen(QPen(self._zone1_color, self.arc_width, cap = Qt.FlatCap))
|
||||
qp.drawArc(rect, segment1*16, span1*16)
|
||||
qp.setPen(QPen(self._zone2_color, self.arc_width, cap = Qt.FlatCap))
|
||||
qp.drawArc(rect, segment2*16, span2*16)
|
||||
|
||||
def draw_gauge(self, qp, event, w):
|
||||
w *= 0.6
|
||||
w = int(w)
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(w, w))
|
||||
rect.moveCenter(event.rect().center())
|
||||
center = QPointF(rect.center())
|
||||
qp.setPen(QPen(Qt.white, self.tick_width, cap = Qt.FlatCap))
|
||||
qp.drawArc(rect, (-45 * 16), (270 * 16))
|
||||
rad = rect.width()/2
|
||||
inc = 270.0 / (self._num_ticks - 1)
|
||||
for i in range(self._num_ticks):
|
||||
p1 = self.qpa[i] * rad
|
||||
p1 += center
|
||||
p2 = self.qpa[i] * (rad + 10)
|
||||
p2 += center
|
||||
line = QLine(p1.toPoint(), p2.toPoint())
|
||||
qp.drawLine(line)
|
||||
|
||||
def draw_digits(self, qp, event, w):
|
||||
w *= 0.8
|
||||
center = event.rect().center()
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(40,18))
|
||||
qp.setPen(QPen(Qt.white, self.tick_width))
|
||||
qp.setFont(QFont('Lato Heavy', self._dial_font_size))
|
||||
rad = w/2
|
||||
inc = 270.0 / (self._num_ticks - 1)
|
||||
for i in range(self._num_ticks):
|
||||
angle = (inc * i) - 225
|
||||
q, r = divmod(self._max_reading * i, self._num_ticks - 1)
|
||||
text = str(q) if r == 0 else ""
|
||||
if text == "": continue
|
||||
x = int(rad * math.cos(math.radians(angle)) + center.x())
|
||||
y = int(rad * math.sin(math.radians(angle)) + center.y())
|
||||
rect.moveCenter(QPoint(x, y))
|
||||
qp.drawText(rect, Qt.AlignCenter, text)
|
||||
|
||||
def draw_center(self, qp, event, w):
|
||||
w *= 0.2
|
||||
w = int(w)
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(w, w))
|
||||
rect.moveCenter(event.rect().center())
|
||||
rad = rect.width()/2
|
||||
cap = QRadialGradient(QPointF(rect.center()), rad)
|
||||
cap.setColorAt(0, self._center_gradient_color)
|
||||
cap.setColorAt(1, self._center_color)
|
||||
qp.setPen(QPen(self._bezel_color, 1.5))
|
||||
qp.setBrush(QBrush(cap))
|
||||
qp.drawEllipse(rect)
|
||||
|
||||
def draw_setpoint(self, qp, event, w):
|
||||
w *= 0.6
|
||||
center = QPointF(event.rect().center())
|
||||
rad = w/2
|
||||
rect = QRect(0, 0, 8, 8)
|
||||
p = self._setpoint * rad
|
||||
p += center
|
||||
rect.moveCenter(p.toPoint())
|
||||
qp.fillRect(rect, QColor(Qt.yellow))
|
||||
|
||||
def draw_needle(self, qp, event, w):
|
||||
w *= 0.6
|
||||
center = event.rect().center()
|
||||
angle = ((self.value * 270.0) / self._max_value) - 225
|
||||
rad = w/2
|
||||
x = int(rad * math.cos(math.radians(angle)) + center.x())
|
||||
y = int(rad * math.sin(math.radians(angle)) + center.y())
|
||||
line = QLine(center.x(), center.y(), x, y)
|
||||
qp.setPen(QPen(Qt.red, 4))
|
||||
qp.drawLine(line)
|
||||
|
||||
def draw_readout(self, qp, event, w):
|
||||
center = event.rect().center()
|
||||
rect = QRect()
|
||||
rect.setSize(QSize(int(w/4), int(w/8)))
|
||||
rect.moveCenter(QPoint(center.x(), center.y() + int(w/4)))
|
||||
text = "{}".format(self.value)
|
||||
qp.setPen(QPen(Qt.white, 4))
|
||||
qp.setFont(QFont('Lato Heavy', self._value_font_size))
|
||||
qp.drawText(rect, Qt.AlignCenter, text)
|
||||
rect.moveCenter(QPoint(center.x(), center.y() + int(w/3)))
|
||||
text = self._gauge_label
|
||||
qp.setFont(QFont('Lato Heavy', self._label_font_size))
|
||||
qp.drawText(rect, Qt.AlignCenter, text)
|
||||
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def update_value(self, value):
|
||||
if value != self.value:
|
||||
self.value = int(value)
|
||||
self.update()
|
||||
|
||||
def set_setpoint(self, value):
|
||||
if value >= self._max_value:
|
||||
angle = 45
|
||||
elif value <= 0:
|
||||
angle = -225
|
||||
else:
|
||||
angle = ((value * 270) / self._max_value) - 225
|
||||
x = math.cos(math.radians(angle))
|
||||
y = math.sin(math.radians(angle))
|
||||
self._setpoint = QPointF(x, y)
|
||||
self.update()
|
||||
|
||||
@Slot(int)
|
||||
def set_threshold(self, value):
|
||||
if value > self._max_value:
|
||||
self._threshold = self._max_value
|
||||
elif value < 0:
|
||||
self._threshold = 0
|
||||
else:
|
||||
self._threshold = value
|
||||
self.update()
|
||||
|
||||
def get_threshold(self):
|
||||
return self._threshold
|
||||
|
||||
def reset_threshold(self):
|
||||
self._threshold = 50
|
||||
|
||||
@Slot(int)
|
||||
def set_max_value(self, value):
|
||||
self._max_value = value
|
||||
|
||||
def get_max_value(self):
|
||||
return self._max_value
|
||||
|
||||
def reset_max_value(self):
|
||||
self._max_value = 100
|
||||
|
||||
@Slot(int)
|
||||
def set_max_reading(self, value):
|
||||
self._max_reading = value
|
||||
self.update()
|
||||
|
||||
def get_max_reading(self):
|
||||
return self._max_reading
|
||||
|
||||
def reset_max_reading(self):
|
||||
self._max_reading = 100
|
||||
|
||||
@Slot(int)
|
||||
def set_num_ticks(self, value):
|
||||
# cannot allow value <= 1 or there will be division by 0 errors
|
||||
self._num_ticks = value if value > 1 else 2
|
||||
self.create_unit_array()
|
||||
self.update()
|
||||
|
||||
def get_num_ticks(self):
|
||||
return self._num_ticks
|
||||
|
||||
def reset_num_ticks(self):
|
||||
self._num_ticks = 11
|
||||
|
||||
@Slot(str)
|
||||
def set_label(self, label):
|
||||
self._gauge_label = label
|
||||
self.update()
|
||||
|
||||
def get_label(self):
|
||||
return self._gauge_label
|
||||
|
||||
def reset_label(self):
|
||||
self._gauge_label = "GAUGE"
|
||||
|
||||
@Slot(QColor)
|
||||
def set_base_gradient_color(self, color):
|
||||
self._base_gradient_color = color
|
||||
self.update()
|
||||
|
||||
def get_base_gradient_color(self):
|
||||
return self._base_gradient_color
|
||||
|
||||
def reset_base_gradient_color(self):
|
||||
self._base_gradient_color = QColor(180, 180, 180)
|
||||
self.update()
|
||||
|
||||
@Slot(QColor)
|
||||
def set_base_color(self, color):
|
||||
self._base_color = color
|
||||
self.update()
|
||||
|
||||
def get_base_color(self):
|
||||
return self._base_color
|
||||
|
||||
def reset_base_color(self):
|
||||
self._base_color = QColor(40, 40, 40)
|
||||
self.update()
|
||||
|
||||
@Slot(QColor)
|
||||
def set_center_gradient_color(self, color):
|
||||
self._center_gradient_color = color
|
||||
self.update()
|
||||
|
||||
def get_center_gradient_color(self):
|
||||
return self._center_gradient_color
|
||||
|
||||
def reset_center_gradient_color(self):
|
||||
self._center_gradient_color = QColor("gray")
|
||||
self.update()
|
||||
|
||||
@Slot(QColor)
|
||||
def set_center_color(self, color):
|
||||
self._center_color = color
|
||||
self.update()
|
||||
|
||||
def get_center_color(self):
|
||||
return self._center_color
|
||||
|
||||
def reset_center_color(self):
|
||||
self._center_color = QColor("#404040")
|
||||
self.update()
|
||||
|
||||
@Slot(QColor)
|
||||
def set_zone2_color(self, color):
|
||||
self._zone2_color = color
|
||||
self.update()
|
||||
|
||||
def get_zone2_color(self):
|
||||
return self._zone2_color
|
||||
|
||||
def reset_zone2_color(self):
|
||||
self._zone2_color = QColor("red")
|
||||
|
||||
@Slot(QColor)
|
||||
def set_zone1_color(self, color):
|
||||
self._zone1_color = color
|
||||
self.update()
|
||||
|
||||
def get_zone1_color(self):
|
||||
return self._zone1_color
|
||||
|
||||
def reset_zone1_color(self):
|
||||
self._zone1_color = QColor("green")
|
||||
|
||||
@Slot(QColor)
|
||||
def set_zone2_color(self, color):
|
||||
self._zone2_color = color
|
||||
self.update()
|
||||
|
||||
def get_zone2_color(self):
|
||||
return self._zone2_color
|
||||
|
||||
def reset_zone2_color(self):
|
||||
self._zone2_color = QColor("red")
|
||||
|
||||
@Slot(QColor)
|
||||
def set_bezel_color(self, color):
|
||||
self._bezel_color = color
|
||||
self.update()
|
||||
|
||||
def get_bezel_color(self):
|
||||
return self._bezel_color
|
||||
|
||||
def reset_bezel_color(self):
|
||||
self._bezel_color = QColor("gray")
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(200, 200)
|
||||
|
||||
def set_halpin_option(self, value):
|
||||
self._halpin_option = value
|
||||
|
||||
def get_halpin_option(self):
|
||||
return self._halpin_option
|
||||
|
||||
def reset_halpin_option(self):
|
||||
self._halpin_option = True
|
||||
|
||||
@Slot(int)
|
||||
def set_bezel_width(self, value):
|
||||
self._bezel_width = value
|
||||
self.update()
|
||||
|
||||
def get_bezel_width(self):
|
||||
return self._bezel_width
|
||||
|
||||
def reset_bezel_width(self):
|
||||
self._bezel_width = 6
|
||||
self.update()
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(200, 200)
|
||||
|
||||
def set_halpin_option(self, value):
|
||||
self._halpin_option = value
|
||||
|
||||
def get_halpin_option(self):
|
||||
return self._halpin_option
|
||||
|
||||
def reset_halpin_option(self):
|
||||
self._halpin_option = True
|
||||
|
||||
halpin_option = Property(bool, get_halpin_option, set_halpin_option, reset_halpin_option)
|
||||
threshold = Property(int, get_threshold, set_threshold, reset_threshold)
|
||||
max_value = Property(int, get_max_value, set_max_value, reset_max_value)
|
||||
max_reading = Property(int, get_max_reading, set_max_reading, reset_max_reading)
|
||||
num_ticks = Property(int, get_num_ticks, set_num_ticks, reset_num_ticks)
|
||||
gauge_label = Property(str, get_label, set_label, reset_label)
|
||||
base_gradient_color = Property(QColor, get_base_gradient_color, set_base_gradient_color, reset_base_gradient_color)
|
||||
base_color = Property(QColor, get_base_color, set_base_color, reset_base_color)
|
||||
center_gradient_color = Property(QColor, get_center_gradient_color, set_center_gradient_color, reset_center_gradient_color)
|
||||
center_color = Property(QColor, get_center_color, set_center_color, reset_center_color)
|
||||
zone1_color = Property(QColor, get_zone1_color, set_zone1_color, reset_zone1_color)
|
||||
zone2_color = Property(QColor, get_zone2_color, set_zone2_color, reset_zone2_color)
|
||||
bezel_color = Property(QColor, get_bezel_color, set_bezel_color, reset_bezel_color)
|
||||
bezel_width = Property(int, get_bezel_width, set_bezel_width, reset_bezel_width)
|
||||
|
||||
#############################
|
||||
# Testing #
|
||||
#############################
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from qtpy.QtWidgets import QSlider, QWidget, QVBoxLayout
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = QWidget()
|
||||
w.setGeometry(100, 100, 400, 400)
|
||||
w.setWindowTitle('Round Gauge')
|
||||
layout = QVBoxLayout(w)
|
||||
gauge = Gauge(w)
|
||||
gauge.set_max_value(20000)
|
||||
gauge.set_max_reading(20)
|
||||
gauge.set_threshold(7200)
|
||||
gauge.set_setpoint(14000)
|
||||
gauge.set_num_ticks(11)
|
||||
gauge.set_label("RPM")
|
||||
gauge._value_font_size = 10
|
||||
gauge._label_font_size = 10
|
||||
gauge._dial_font_size = 10
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(20000)
|
||||
slider.setSingleStep(10)
|
||||
slider.setPageStep(100)
|
||||
slider.valueChanged.connect(gauge.update_value)
|
||||
layout.addWidget(gauge)
|
||||
layout.addWidget(slider)
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
327
lib/python/qtvcp/widgets/round_progress.py
Normal file
@@ -0,0 +1,327 @@
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
class RoundProgressBar(QWidget):
|
||||
|
||||
StyleDonut = 1
|
||||
StylePie = 2
|
||||
StyleLine = 3
|
||||
|
||||
PositionLeft = 180
|
||||
PositionTop = 90
|
||||
PositionRight = 0
|
||||
PositionBottom = -90
|
||||
|
||||
UF_VALUE = 1
|
||||
UF_PERCENT = 2
|
||||
UF_MAX = 4
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(RoundProgressBar,self).__init__(parent)
|
||||
self.min = 0
|
||||
self.max = 100
|
||||
self.value = 25
|
||||
|
||||
self.nullPosition = self.PositionTop
|
||||
self.barStyle = self.StyleDonut
|
||||
self.outlinePenWidth =1
|
||||
self.dataPenWidth = 1
|
||||
self.rebuildBrush = False
|
||||
self.format = "%p%"
|
||||
self.decimals = 1
|
||||
self.updateFlags = self.UF_PERCENT
|
||||
self.gradientData = []
|
||||
self.donutThicknessRatio = 0.75
|
||||
self.setDataColors([(0., QColor.fromRgb(255,0,0)), (0.5, QColor.fromRgb(255,255,0)), (1., QColor.fromRgb(0,255,0))])
|
||||
|
||||
def setRange(self, min, max):
|
||||
self.min = min
|
||||
self.max = max
|
||||
|
||||
if self.max < self.min:
|
||||
self.max, self.min = self.min, self.max
|
||||
|
||||
if self.value < self.min:
|
||||
self.value = self.min
|
||||
elif self.value > self.max:
|
||||
self.value = self.max
|
||||
|
||||
if not self.gradientData:
|
||||
self.rebuildBrush = True
|
||||
self.update()
|
||||
|
||||
def setMinimum(self, min):
|
||||
self.setRange(min, self.max)
|
||||
|
||||
def setMaximum(self, max):
|
||||
self.setRange(self.min, max)
|
||||
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(100, 100)
|
||||
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def setValue(self, val):
|
||||
if self.value != val:
|
||||
if val < self.min:
|
||||
self.value = self.min
|
||||
elif val > self.max:
|
||||
self.value = self.max
|
||||
else:
|
||||
self.value = val
|
||||
self.update()
|
||||
|
||||
def setNullPosition(self, position):
|
||||
if position != self.nullPosition:
|
||||
self.nullPosition = position
|
||||
if not self.gradientData:
|
||||
self.rebuildBrush = True
|
||||
self.update()
|
||||
|
||||
def setBarStyle(self, style):
|
||||
if style != self.barStyle:
|
||||
self.barStyle = style
|
||||
self.update()
|
||||
|
||||
def setOutlinePenWidth(self, penWidth):
|
||||
if penWidth != self.outlinePenWidth:
|
||||
self.outlinePenWidth = penWidth
|
||||
self.update()
|
||||
|
||||
def setDataPenWidth(self, penWidth):
|
||||
if penWidth != self.dataPenWidth:
|
||||
self.dataPenWidth = penWidth
|
||||
self.update()
|
||||
|
||||
def setDataColors(self, stopPoints):
|
||||
if stopPoints != self.gradientData:
|
||||
self.gradientData = stopPoints
|
||||
self.rebuildBrush = True
|
||||
self.update()
|
||||
|
||||
def setFormat(self, format):
|
||||
if format != self.format:
|
||||
self.format = format
|
||||
self.valueFormatChanged()
|
||||
|
||||
def resetFormat(self):
|
||||
self.format = ''
|
||||
self.valueFormatChanged()
|
||||
|
||||
def setDecimals(self, count):
|
||||
if count >= 0 and count != self.decimals:
|
||||
self.decimals = count
|
||||
self.valueFormatChanged()
|
||||
|
||||
def setDonutThicknessRatio(self, val):
|
||||
self.donutThicknessRatio = max(0., min(val, 1.))
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
outerRadius = min(self.width(), self.height())
|
||||
baseRect = QtCore.QRectF(1, 1, outerRadius-2, outerRadius-2)
|
||||
|
||||
buffer = QtGui.QImage(outerRadius, outerRadius, QtGui.QImage.Format_ARGB32)
|
||||
buffer.fill(0)
|
||||
|
||||
p = QtGui.QPainter(buffer)
|
||||
p.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
||||
# data brush
|
||||
self.rebuildDataBrushIfNeeded()
|
||||
|
||||
# background
|
||||
self.drawBackground(p, buffer.rect())
|
||||
|
||||
# base circle
|
||||
self.drawBase(p, baseRect)
|
||||
|
||||
# data circle
|
||||
arcStep = 360.0 / (self.max - self.min) * self.value
|
||||
self.drawValue(p, baseRect, self.value, arcStep)
|
||||
|
||||
# center circle
|
||||
innerRect, innerRadius = self.calculateInnerRect(baseRect, outerRadius)
|
||||
self.drawInnerBackground(p, innerRect)
|
||||
|
||||
# text
|
||||
self.drawText(p, innerRect, innerRadius, self.value)
|
||||
|
||||
# finally draw the bar
|
||||
p.end()
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.drawImage(0, 0, buffer)
|
||||
|
||||
def drawBackground(self, p, baseRect):
|
||||
p.fillRect(baseRect, self.palette().window())
|
||||
|
||||
def drawBase(self, p, baseRect):
|
||||
bs = self.barStyle
|
||||
if bs == self.StyleDonut:
|
||||
p.setPen(QtGui.QPen(self.palette().shadow().color(), self.outlinePenWidth))
|
||||
p.setBrush(self.palette().base())
|
||||
p.drawEllipse(baseRect)
|
||||
elif bs == self.StylePie:
|
||||
p.setPen(QtGui.QPen(self.palette().base().color(), self.outlinePenWidth))
|
||||
p.setBrush(self.palette().base())
|
||||
p.drawEllipse(baseRect)
|
||||
elif bs == self.StyleLine:
|
||||
p.setPen(QtGui.QPen(self.palette().base().color(), self.outlinePenWidth))
|
||||
p.setBrush(QtCore.Qt.NoBrush)
|
||||
p.drawEllipse(baseRect.adjusted(self.outlinePenWidth/2, self.outlinePenWidth/2, -self.outlinePenWidth/2, -self.outlinePenWidth/2))
|
||||
|
||||
def drawValue(self, p, baseRect, value, arcLength):
|
||||
# nothing to draw
|
||||
if value == self.min:
|
||||
return
|
||||
|
||||
# for Line style
|
||||
if self.barStyle == self.StyleLine:
|
||||
p.setPen(QtGui.QPen(self.palette().highlight().color(), self.dataPenWidth))
|
||||
p.setBrush(QtCore.Qt.NoBrush)
|
||||
p.drawArc(baseRect.adjusted(self.outlinePenWidth/2, self.outlinePenWidth/2, -self.outlinePenWidth/2, -self.outlinePenWidth/2),
|
||||
self.nullPosition * 16,
|
||||
-arcLength * 16)
|
||||
return
|
||||
|
||||
# for Pie and Donut styles
|
||||
dataPath = QtGui.QPainterPath()
|
||||
dataPath.setFillRule(QtCore.Qt.WindingFill)
|
||||
|
||||
# pie segment outer
|
||||
dataPath.moveTo(baseRect.center())
|
||||
dataPath.arcTo(baseRect, self.nullPosition, -arcLength)
|
||||
dataPath.lineTo(baseRect.center())
|
||||
|
||||
p.setBrush(self.palette().highlight())
|
||||
p.setPen(QtGui.QPen(self.palette().shadow().color(), self.dataPenWidth))
|
||||
p.drawPath(dataPath)
|
||||
|
||||
def calculateInnerRect(self, baseRect, outerRadius):
|
||||
# for Line style
|
||||
if self.barStyle == self.StyleLine:
|
||||
innerRadius = outerRadius - self.outlinePenWidth
|
||||
else: # for Pie and Donut styles
|
||||
innerRadius = outerRadius * self.donutThicknessRatio
|
||||
|
||||
delta = (outerRadius - innerRadius) / 2.
|
||||
innerRect = QtCore.QRectF(delta, delta, innerRadius, innerRadius)
|
||||
return innerRect, innerRadius
|
||||
|
||||
def drawInnerBackground(self, p, innerRect):
|
||||
if self.barStyle == self.StyleDonut:
|
||||
p.setBrush(self.palette().alternateBase())
|
||||
|
||||
cmod = p.compositionMode()
|
||||
p.setCompositionMode(QtGui.QPainter.CompositionMode_Source)
|
||||
|
||||
p.drawEllipse(innerRect)
|
||||
|
||||
p.setCompositionMode(cmod)
|
||||
|
||||
def drawText(self, p, innerRect, innerRadius, value):
|
||||
if not self.format:
|
||||
return
|
||||
|
||||
text = self.valueToText(value)
|
||||
|
||||
# !!! to revise
|
||||
f = self.font()
|
||||
# f.setPixelSize(innerRadius * max(0.05, (0.35 - self.decimals * 0.08)))
|
||||
f.setPixelSize(int(innerRadius * 1.8 / len(text)))
|
||||
p.setFont(f)
|
||||
|
||||
textRect = innerRect
|
||||
p.setPen(self.palette().text().color())
|
||||
p.drawText(textRect, QtCore.Qt.AlignCenter, text)
|
||||
|
||||
def valueToText(self, value):
|
||||
textToDraw = self.format
|
||||
|
||||
format_string = '{' + ':.{}f'.format(self.decimals) + '}'
|
||||
|
||||
if self.updateFlags & self.UF_VALUE:
|
||||
textToDraw = textToDraw.replace("%v", format_string.format(value))
|
||||
|
||||
if self.updateFlags & self.UF_PERCENT:
|
||||
percent = (value - self.min) / (self.max - self.min) * 100.0
|
||||
textToDraw = textToDraw.replace("%p", format_string.format(percent))
|
||||
|
||||
if self.updateFlags & self.UF_MAX:
|
||||
m = self.max - self.min + 1
|
||||
textToDraw = textToDraw.replace("%m", format_string.format(m))
|
||||
|
||||
return textToDraw
|
||||
|
||||
def valueFormatChanged(self):
|
||||
self.updateFlags = 0;
|
||||
|
||||
if "%v" in self.format:
|
||||
self.updateFlags |= self.UF_VALUE
|
||||
|
||||
if "%p" in self.format:
|
||||
self.updateFlags |= self.UF_PERCENT
|
||||
|
||||
if "%m" in self.format:
|
||||
self.updateFlags |= self.UF_MAX
|
||||
|
||||
self.update()
|
||||
|
||||
def rebuildDataBrushIfNeeded(self):
|
||||
if self.rebuildBrush:
|
||||
self.rebuildBrush = False
|
||||
|
||||
dataBrush = QtGui.QConicalGradient()
|
||||
dataBrush.setCenter(0.5,0.5)
|
||||
dataBrush.setCoordinateMode(QtGui.QGradient.StretchToDeviceMode)
|
||||
|
||||
for pos, color in self.gradientData:
|
||||
dataBrush.setColorAt(1.0 - pos, color)
|
||||
|
||||
# angle
|
||||
dataBrush.setAngle(self.nullPosition)
|
||||
|
||||
p = self.palette()
|
||||
p.setBrush(QtGui.QPalette.Highlight, dataBrush)
|
||||
self.setPalette(p)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from qtpy.QtWidgets import QDial, QWidget, QHBoxLayout, QApplication
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
w = QWidget()
|
||||
#w.setWindowFlags( Qt.FramelessWindowHint | Qt.Dialog | Qt.WindowStaysOnTopHint )
|
||||
w.setGeometry(300, 300, 250, 150)
|
||||
w.setWindowTitle('Test')
|
||||
|
||||
h = QHBoxLayout(w)
|
||||
|
||||
qd = QDial(w)
|
||||
|
||||
gui = RoundProgressBar(w)
|
||||
gui.setBarStyle(gui.StyleDonut)
|
||||
#gui.setBarStyle(gui.StylePie)
|
||||
#gui.setBarStyle(gui.StyleLine)
|
||||
gui.setDataColors([(0., QColor.fromRgb(255,0,0)), (0.5, QColor.fromRgb(255,255,0)), (1., QColor.fromRgb(0,255,0))])
|
||||
gui.setMaximum(100)
|
||||
gui.setValue(70)
|
||||
gui.setDataPenWidth(3)
|
||||
gui.setOutlinePenWidth(3)
|
||||
gui.setDecimals(1)
|
||||
gui.setFormat('%v')
|
||||
gui.setRange(0, 100)
|
||||
|
||||
qd.valueChanged.connect(gui.setValue)
|
||||
|
||||
h.addWidget(qd)
|
||||
h.addWidget(gui)
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
|
||||
84
lib/python/qtvcp/widgets/runFromLine_dialog.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
#
|
||||
# Copyright (c) 2018 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# a probe screen based on Versa probe screen
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from qtpy import QtWidgets, uic
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
class RunFromLineDialog(QtWidgets.QDialog, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(RunFromLineDialog, self).__init__(parent)
|
||||
# Load the widgets UI file:
|
||||
self.filename = os.path.join(PATH.SHAREDIR,'widgets_ui', 'runFromLine_dialog.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_test():
|
||||
return (self.probe_enable and STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_test()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(homed_on_test()))
|
||||
|
||||
def accept(self):
|
||||
if self.radioButton_cw.isChecked():
|
||||
direction = 'M3'
|
||||
else:
|
||||
direction = 'M4'
|
||||
speed = self.spinBox_rpm.value()
|
||||
|
||||
ACTION.CALL_MDI('s{} {}'.format(speed,direction))
|
||||
super(RunFromLineDialog, self).accept()
|
||||
|
||||
########################################
|
||||
# required boiler code
|
||||
########################################
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
####################################
|
||||
# Testing
|
||||
####################################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = RunFromLineDialog()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
1248
lib/python/qtvcp/widgets/screen_options.py
Normal file
607
lib/python/qtvcp/widgets/simple_widgets.py
Normal file
@@ -0,0 +1,607 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtVcp simple widgets
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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)
|
||||
|
||||
185
lib/python/qtvcp/widgets/state_label.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
# QTVcp Widget
|
||||
#
|
||||
# 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.
|
||||
###############################################################################
|
||||
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
from qtvcp.widgets.simple_widgets import ScaledLabel
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
LOG = logger.getLogger(__name__)
|
||||
INFO = Info()
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class StateLabel(ScaledLabel, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StateLabel, self).__init__(parent)
|
||||
|
||||
self._true_textTemplate = 'True'
|
||||
self._false_textTemplate = 'False'
|
||||
self.machine_units = False
|
||||
self.metric_mode = True
|
||||
self.css_mode = False
|
||||
self.fpr_mode = False
|
||||
self.diameter_mode = False
|
||||
|
||||
def _hal_init(self):
|
||||
def _f(data):
|
||||
self._set_text(data)
|
||||
if self.metric_mode:
|
||||
STATUS.connect('metric-mode-changed', lambda w, data: _f(data))
|
||||
elif self.css_mode:
|
||||
STATUS.connect('css-mode', lambda w, data: _f(data))
|
||||
elif self.fpr_mode:
|
||||
STATUS.connect('fpr-mode', lambda w, data: _f(data))
|
||||
elif self.diameter_mode:
|
||||
STATUS.connect('diameter-mode', lambda w, data: _f(data))
|
||||
elif self.machine_units:
|
||||
self._set_text(INFO.MACHINE_IS_METRIC)
|
||||
|
||||
def _set_text(self, data):
|
||||
if data:
|
||||
self.setText(self._true_textTemplate)
|
||||
else:
|
||||
self.setText(self._false_textTemplate)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('machine_is_metric', 'metric_mode', 'css_mode', 'fpr_mode',
|
||||
'diameter_mode')
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i + '_status'] = False
|
||||
|
||||
# property getter/setters
|
||||
|
||||
def set_true_textTemplate(self, data):
|
||||
self._true_textTemplate = data
|
||||
try:
|
||||
self._set_text(True)
|
||||
except Exception as e:
|
||||
LOG.exception("textTemplate: {}, Data: {}".format(self._textTemplate, data), exc_info=e)
|
||||
self.setText('Error')
|
||||
def get_true_textTemplate(self):
|
||||
return self._true_textTemplate
|
||||
def reset_true_textTemplate(self):
|
||||
self._true_textTemplate = '%s'
|
||||
|
||||
def set_false_textTemplate(self, data):
|
||||
self._false_textTemplate = data
|
||||
try:
|
||||
self._set_text(False)
|
||||
except:
|
||||
self.setText('Error 2')
|
||||
def get_false_textTemplate(self):
|
||||
return self._false_textTemplate
|
||||
def reset_false_textTemplate(self):
|
||||
self._false_textTemplate = '%s'
|
||||
|
||||
# metric unit status
|
||||
def set_machine_units(self, data):
|
||||
self.machine_units = data
|
||||
if data:
|
||||
self._toggle_properties('machine_is_metric')
|
||||
def get_machine_units(self):
|
||||
return self.machine_units
|
||||
def reset_machine_units(self):
|
||||
self.machine_units = True
|
||||
|
||||
# metric mode status
|
||||
def set_metric_mode(self, data):
|
||||
self.metric_mode = data
|
||||
if data:
|
||||
self._toggle_properties('metric_mode')
|
||||
def get_metric_mode(self):
|
||||
return self.metric_mode
|
||||
def reset_metric_mode(self):
|
||||
self.metric_mode = True
|
||||
|
||||
# css mode status
|
||||
def set_css_mode(self, data):
|
||||
self.css_mode = data
|
||||
if data:
|
||||
self._toggle_properties('css_mode')
|
||||
def get_css_mode(self):
|
||||
return self.css_mode
|
||||
def reset_css_mode(self):
|
||||
self.css_mode = True
|
||||
|
||||
# fpr mode status
|
||||
def set_fpr_mode(self, data):
|
||||
self.fpr_mode = data
|
||||
if data:
|
||||
self._toggle_properties('fpr_mode')
|
||||
def get_fpr_mode(self):
|
||||
return self.fpr_mode
|
||||
def reset_fpr_mode(self):
|
||||
self.fpr_mode = True
|
||||
|
||||
# diameter mode status
|
||||
def set_diameter_mode(self, data):
|
||||
self.diameter_mode = data
|
||||
if data:
|
||||
self._toggle_properties('diameter_mode')
|
||||
def get_diameter_mode(self):
|
||||
return self.diameter_mode
|
||||
def reset_diameter_mode(self):
|
||||
self.diameter_mode = True
|
||||
|
||||
# designer will show these properties in this order:
|
||||
# BOOL
|
||||
machine_is_metric_status = QtCore.Property(bool, get_machine_units, set_machine_units, reset_machine_units)
|
||||
metric_mode_status = QtCore.Property(bool, get_metric_mode, set_metric_mode, reset_metric_mode)
|
||||
css_mode_status = QtCore.Property(bool, get_css_mode, set_css_mode, reset_css_mode)
|
||||
fpr_mode_status = QtCore.Property(bool, get_fpr_mode, set_fpr_mode, reset_fpr_mode)
|
||||
diameter_mode_status = QtCore.Property(bool, get_diameter_mode, set_diameter_mode, reset_diameter_mode)
|
||||
|
||||
# Non BOOL
|
||||
true_textTemplate = QtCore.Property(str, get_true_textTemplate,
|
||||
set_true_textTemplate, reset_true_textTemplate)
|
||||
false_textTemplate = QtCore.Property(str, get_false_textTemplate,
|
||||
set_false_textTemplate, reset_false_textTemplate)
|
||||
|
||||
# boilder code
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
label = Lcnc_STATUS_Bool_Label()
|
||||
label.show()
|
||||
sys.exit(app.exec())
|
||||
564
lib/python/qtvcp/widgets/state_led.py
Normal file
@@ -0,0 +1,564 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp widget
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
#################################################################################
|
||||
|
||||
from qtpy.QtCore import Property
|
||||
import hal
|
||||
from qtvcp.widgets.led_widget import LED
|
||||
from qtvcp.core import Status
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class StateLED(LED):
|
||||
'''
|
||||
|
||||
Widget to display a colored LED based on a linuxcnc state.
|
||||
'''
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(StateLED, self).__init__(parent)
|
||||
self.setState(False)
|
||||
|
||||
self.is_estopped = False
|
||||
self.is_on = False
|
||||
self.is_homed = False
|
||||
self.is_idle = False
|
||||
self.is_paused = False
|
||||
self.invert_state = False
|
||||
self.is_flood = False
|
||||
self.is_mist = False
|
||||
self.is_block_delete = False
|
||||
self.is_optional_stop = False
|
||||
self.is_joint_homed = False
|
||||
self.is_limits_overridden = False
|
||||
self.is_manual = False
|
||||
self.is_mdi = False
|
||||
self.is_auto = False
|
||||
self.is_spindle_stopped = False
|
||||
self.is_spindle_fwd = False
|
||||
self.is_spindle_rev = False
|
||||
self.is_spindle_at_speed = False
|
||||
self.is_neg_limit_tripped = False
|
||||
self.is_pos_limit_tripped = False
|
||||
self.is_limits_tripped = False
|
||||
self._halpin_option = True
|
||||
self._follow_halpin = False
|
||||
|
||||
self.joint_number = 0
|
||||
self._halpin_name = ''
|
||||
|
||||
self._override = 1
|
||||
self._at_speed_percent = .1
|
||||
|
||||
def _hal_init(self):
|
||||
def only_false(data):
|
||||
if data:
|
||||
return
|
||||
self._flip_state(False)
|
||||
# optional output HAL pin reflecting state
|
||||
if self._halpin_option:
|
||||
if self._halpin_name == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._halpin_name
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_BIT, hal.HAL_OUT)
|
||||
if self.is_estopped:
|
||||
STATUS.connect('state-estop', lambda w: self._flip_state(True))
|
||||
STATUS.connect('state-estop-reset', lambda w: self._flip_state(False))
|
||||
elif self.is_on:
|
||||
STATUS.connect('state-on', lambda w: self._flip_state(True))
|
||||
STATUS.connect('state-off', lambda w: self._flip_state(False))
|
||||
elif self.is_homed:
|
||||
STATUS.connect('all-homed', lambda w: self._flip_state(True))
|
||||
STATUS.connect('not-all-homed', lambda w, axis: self._flip_state(False))
|
||||
elif self.is_idle:
|
||||
STATUS.connect('interp-idle', lambda w: self._flip_state(True))
|
||||
STATUS.connect('interp-run', lambda w: self._flip_state(False))
|
||||
elif self.is_paused:
|
||||
STATUS.connect('program-pause-changed', lambda w, data: self._flip_state(data))
|
||||
elif self.is_flood:
|
||||
STATUS.connect('flood-changed', lambda w, data: self._flip_state(data))
|
||||
elif self.is_mist:
|
||||
STATUS.connect('mist-changed', lambda w, data: self._flip_state(data))
|
||||
elif self.is_block_delete:
|
||||
STATUS.connect('block-delete-changed', lambda w, data: self._flip_state(data))
|
||||
elif self.is_optional_stop:
|
||||
STATUS.connect('optional-stop-changed', lambda w, data: self._flip_state(data))
|
||||
elif self.is_joint_homed:
|
||||
STATUS.connect('homed', lambda w, data: self.joint_homed(data))
|
||||
STATUS.connect('not-all-homed', lambda w, data: self.joints_unhomed(data))
|
||||
elif self.is_limits_overridden:
|
||||
STATUS.connect('override-limits-changed', self.check_override_limits)
|
||||
STATUS.connect('hard-limits-tripped', lambda w, data, group: only_false(data))
|
||||
elif self.is_manual or self.is_mdi or self.is_auto:
|
||||
STATUS.connect('mode-manual', lambda w: self.mode_changed(0))
|
||||
STATUS.connect('mode-mdi', lambda w: self.mode_changed(1))
|
||||
STATUS.connect('mode-auto', lambda w: self.mode_changed(2))
|
||||
elif self.is_spindle_stopped or self.is_spindle_fwd or self.is_spindle_rev:
|
||||
STATUS.connect('spindle-control-changed', lambda w, num, state, speed, upto: self.spindle_changed(speed))
|
||||
elif self.is_spindle_at_speed:
|
||||
STATUS.connect('requested-spindle-speed-changed', lambda w, speed: self.spindle_requested_changed(speed))
|
||||
STATUS.connect('spindle-override-changed', lambda w, rate: self.spindle_override_changed(rate))
|
||||
STATUS.connect('actual-spindle-speed-changed',lambda w, speed: self.spindle_actual_changed(speed))
|
||||
elif self.is_neg_limit_tripped or \
|
||||
self.is_pos_limit_tripped or \
|
||||
self.is_limits_tripped:
|
||||
STATUS.connect('hard-limits-tripped', lambda w, data, group: self.switch_on_hard_limits(data, group))
|
||||
elif self._follow_halpin:
|
||||
STATUS.connect('periodic', lambda w: self._set_halpin_text())
|
||||
|
||||
def _flip_state(self, data):
|
||||
if self.invert_state:
|
||||
data = not data
|
||||
self.change_state(data)
|
||||
|
||||
def change_state(self, state):
|
||||
super(StateLED, self).change_state(state)
|
||||
if self._halpin_option:
|
||||
self.hal_pin.set(state)
|
||||
|
||||
def joint_homed(self, joint):
|
||||
if int(joint) == self.joint_number:
|
||||
self._flip_state(True)
|
||||
|
||||
def joints_unhomed(self, jlist):
|
||||
if str(self.joint_number) in jlist:
|
||||
self._flip_state(False)
|
||||
|
||||
def check_override_limits(self, w, state, data):
|
||||
for i in data:
|
||||
if i == 1:
|
||||
self._flip_state(True)
|
||||
return
|
||||
self._flip_state(False)
|
||||
|
||||
def mode_changed(self, mode):
|
||||
if self.is_manual and mode == 0:
|
||||
self._flip_state(True)
|
||||
elif self.is_mdi and mode == 1:
|
||||
self._flip_state(True)
|
||||
elif self.is_auto and mode == 2:
|
||||
self._flip_state(True)
|
||||
else:
|
||||
self._flip_state(False)
|
||||
|
||||
def spindle_changed(self, state):
|
||||
if self.is_spindle_stopped and state == 0:
|
||||
self._flip_state(True)
|
||||
elif self.is_spindle_rev and state < 0:
|
||||
self._flip_state(True)
|
||||
elif self.is_spindle_fwd and state > 0:
|
||||
self._flip_state(True)
|
||||
else:
|
||||
self._flip_state(False)
|
||||
|
||||
def spindle_off(self, state):
|
||||
if state == 0:
|
||||
if self.invert_state:
|
||||
self.change_state(True)
|
||||
else:
|
||||
self.change_state(False)
|
||||
|
||||
def spindle_requested_changed(self, speed):
|
||||
self._requested = speed
|
||||
state = STATUS.is_spindle_on()
|
||||
self.setState(state)
|
||||
|
||||
def spindle_override_changed(self, rate):
|
||||
self._override = rate/100.0
|
||||
|
||||
def spindle_actual_changed(self, speed):
|
||||
self._actual = speed
|
||||
if not STATUS.is_spindle_on():
|
||||
if self._halpin_option:
|
||||
self.hal_pin.set(False)
|
||||
self.setFlashing(False)
|
||||
return
|
||||
flash = self.spindle_near_check()
|
||||
self.setFlashing(flash)
|
||||
if self._halpin_option:
|
||||
self.hal_pin.set(not flash)
|
||||
|
||||
def spindle_near_check(self):
|
||||
req = self._requested * self._override
|
||||
upper = abs(req * (1+self._at_speed_percent))
|
||||
lower = abs(req * (1-self._at_speed_percent))
|
||||
value = abs(self._actual)
|
||||
#print(req,lower,value,upper,lower <= value <= upper)
|
||||
if lower <= value <= upper:
|
||||
return False
|
||||
return True
|
||||
|
||||
def switch_on_hard_limits(self, data, group):
|
||||
'''
|
||||
Switch LED state based on hard limits state
|
||||
of selected joint.
|
||||
|
||||
Args:
|
||||
data (bool): combined limits state.
|
||||
group (list): list of all joints negative
|
||||
and positive hard limits state.
|
||||
'''
|
||||
|
||||
pair = group[self.joint_number]
|
||||
if self.is_neg_limit_tripped: data = pair[0]
|
||||
elif self.is_pos_limit_tripped: data = pair[1]
|
||||
else:
|
||||
data = pair[0]+pair[1]
|
||||
self._flip_state(data)
|
||||
|
||||
def _set_halpin_text(self):
|
||||
try:
|
||||
state = bool(self.HAL_GCOMP_.hal.get_value(self._halpin_name))
|
||||
except Exception as e:
|
||||
return
|
||||
self._flip_state(state)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('is_paused', 'is_estopped', 'is_on', 'is_idle', 'is_homed',
|
||||
'is_flood', 'is_mist', 'is_block_delete', 'is_optional_stop',
|
||||
'is_joint_homed', 'is_limits_overridden','is_manual',
|
||||
'is_mdi', 'is_auto', 'is_spindle_stopped', 'is_spindle_fwd',
|
||||
'is_spindle_rev','is_spindle_at_speed','neg_hard_limit_state',
|
||||
'pos_hard_limit_state','hard_limits_state','followhalpin')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
if i in('neg_hard_limit_state',
|
||||
'pos_hard_limit_state','hard_limits_state'):
|
||||
self[i] = False
|
||||
else:
|
||||
self[i+'_status'] = False
|
||||
|
||||
# property getter/setters
|
||||
|
||||
# invert status
|
||||
def set_invert_state(self, data):
|
||||
self.invert_state = data
|
||||
def get_invert_state(self):
|
||||
return self.invert_state
|
||||
def reset_invert_state(self):
|
||||
self.invert_state = False
|
||||
|
||||
# machine is paused status
|
||||
def set_is_paused(self, data):
|
||||
self.is_paused = data
|
||||
if data:
|
||||
self._toggle_properties('is_paused')
|
||||
def get_is_paused(self):
|
||||
return self.is_paused
|
||||
def reset_is_paused(self):
|
||||
self.is_paused = False
|
||||
|
||||
# machine is estopped status
|
||||
def set_is_estopped(self, data):
|
||||
self.is_estopped = data
|
||||
if data:
|
||||
self._toggle_properties('is_estopped')
|
||||
def get_is_estopped(self):
|
||||
return self.is_estopped
|
||||
def reset_is_estopped(self):
|
||||
self.is_estopped = False
|
||||
|
||||
# machine is on status
|
||||
def set_is_on(self, data):
|
||||
self.is_on = data
|
||||
if data:
|
||||
self._toggle_properties('is_on')
|
||||
def get_is_on(self):
|
||||
return self.is_on
|
||||
def reset_is_on(self):
|
||||
self.is_on = False
|
||||
|
||||
# machine is idle status
|
||||
def set_is_idle(self, data):
|
||||
self.is_idle = data
|
||||
if data:
|
||||
self._toggle_properties('is_idle')
|
||||
def get_is_idle(self):
|
||||
return self.is_idle
|
||||
def reset_is_idle(self):
|
||||
self.is_idle = False
|
||||
|
||||
# machine_is_homed status
|
||||
def set_is_homed(self, data):
|
||||
self.is_homed = data
|
||||
if data:
|
||||
self._toggle_properties('is_homed')
|
||||
def get_is_homed(self):
|
||||
return self.is_homed
|
||||
def reset_is_homed(self):
|
||||
self.is_homed = False
|
||||
|
||||
# machine is_flood status
|
||||
def set_is_flood(self, data):
|
||||
self.is_flood = data
|
||||
if data:
|
||||
self._toggle_properties('is_flood')
|
||||
def get_is_flood(self):
|
||||
return self.is_flood
|
||||
def reset_is_flood(self):
|
||||
self.is_flood = False
|
||||
|
||||
# machine is_mist status
|
||||
def set_is_mist(self, data):
|
||||
self.is_mist = data
|
||||
if data:
|
||||
self._toggle_properties('is_mist')
|
||||
def get_is_mist(self):
|
||||
return self.is_mist
|
||||
def reset_is_mist(self):
|
||||
self.is_mist = False
|
||||
|
||||
# machine_is_block_delete status
|
||||
def set_is_block_delete(self, data):
|
||||
self.is_block_delete = data
|
||||
if data:
|
||||
self._toggle_properties('is_block_delete')
|
||||
def get_is_block_delete(self):
|
||||
return self.is_block_delete
|
||||
def reset_is_block_delete(self):
|
||||
self.is_block_delete = False
|
||||
|
||||
# machine_is_optional_stop status
|
||||
def set_is_optional_stop(self, data):
|
||||
self.is_optional_stop = data
|
||||
if data:
|
||||
self._toggle_properties('is_optional_stop')
|
||||
def get_is_optional_stop(self):
|
||||
return self.is_optional_stop
|
||||
def reset_is_optional_stop(self):
|
||||
self.is_optional_stop = False
|
||||
|
||||
# machine_is_joint_homed status
|
||||
def set_is_joint_homed(self, data):
|
||||
self.is_joint_homed = data
|
||||
if data:
|
||||
self._toggle_properties('is_joint_homed')
|
||||
def get_is_joint_homed(self):
|
||||
return self.is_joint_homed
|
||||
def reset_is_joint_homed(self):
|
||||
self.is_joint_homed = False
|
||||
|
||||
# machine_is_limits_overridden status
|
||||
def set_is_limits_overridden(self, data):
|
||||
self.is_limits_overridden = data
|
||||
if data:
|
||||
self._toggle_properties('is_limits_overridden')
|
||||
def get_is_limits_overridden(self):
|
||||
return self.is_limits_overridden
|
||||
def reset_is_limits_overridden(self):
|
||||
self.is_limits_overridden = False
|
||||
|
||||
# machine is manual status
|
||||
def set_is_manual(self, data):
|
||||
self.is_manual = data
|
||||
if data:
|
||||
self._toggle_properties('is_manual')
|
||||
def get_is_manual(self):
|
||||
return self.is_manual
|
||||
def reset_is_manual(self):
|
||||
self.is_manual = False
|
||||
|
||||
# machine is mdi status
|
||||
def set_is_mdi(self, data):
|
||||
self.is_mdi = data
|
||||
if data:
|
||||
self._toggle_properties('is_mdi')
|
||||
def get_is_mdi(self):
|
||||
return self.is_mdi
|
||||
def reset_is_mdi(self):
|
||||
self.is_mdi = False
|
||||
|
||||
# machine is auto status
|
||||
def set_is_auto(self, data):
|
||||
self.is_auto = data
|
||||
if data:
|
||||
self._toggle_properties('is_auto')
|
||||
def get_is_auto(self):
|
||||
return self.is_auto
|
||||
def reset_is_auto(self):
|
||||
self.is_auto = False
|
||||
|
||||
# machine is spindle_stopped status
|
||||
def set_is_spindle_stopped(self, data):
|
||||
self.is_spindle_stopped = data
|
||||
if data:
|
||||
self._toggle_properties('is_spindle_stopped')
|
||||
def get_is_spindle_stopped(self):
|
||||
return self.is_spindle_stopped
|
||||
def reset_is_spindle_stopped(self):
|
||||
self.is_spindle_stopped = False
|
||||
|
||||
# machine is spindle_fwd status
|
||||
def set_is_spindle_fwd(self, data):
|
||||
self.is_spindle_fwd = data
|
||||
if data:
|
||||
self._toggle_properties('is_spindle_fwd')
|
||||
def get_is_spindle_fwd(self):
|
||||
return self.is_spindle_fwd
|
||||
def reset_is_spindle_fwd(self):
|
||||
self.is_spindle_fwd = False
|
||||
|
||||
# machine is spindle_rev status
|
||||
def set_is_spindle_rev(self, data):
|
||||
self.is_spindle_rev = data
|
||||
if data:
|
||||
self._toggle_properties('is_spindle_rev')
|
||||
def get_is_spindle_rev(self):
|
||||
return self.is_spindle_rev
|
||||
def reset_is_spindle_rev(self):
|
||||
self.is_spindle_rev = False
|
||||
|
||||
# machine is spindle_at_speed status
|
||||
def set_is_spindle_at_speed(self, data):
|
||||
self.is_spindle_at_speed = data
|
||||
if data:
|
||||
self._toggle_properties('is_spindle_at_speed')
|
||||
def get_is_spindle_at_speed(self):
|
||||
return self.is_spindle_at_speed
|
||||
def reset_is_spindle_at_speed(self):
|
||||
self.is_spindle_at_speed = False
|
||||
|
||||
# machine hard limits_tripped status
|
||||
def set_neg_limit_tripped(self, data):
|
||||
self.is_neg_limit_tripped = data
|
||||
if data:
|
||||
self._toggle_properties('neg_hard_limit_state')
|
||||
def get_neg_limit_tripped(self):
|
||||
return self.is_neg_limit_tripped
|
||||
def reset_neg_limit_tripped(self):
|
||||
self.is_neg_limit_tripped = False
|
||||
|
||||
# machine hard limits_tripped status
|
||||
def set_pos_limit_tripped(self, data):
|
||||
self.is_pos_limit_tripped = data
|
||||
if data:
|
||||
self._toggle_properties('pos_hard_limit_state')
|
||||
def get_pos_limit_tripped(self):
|
||||
return self.is_pos_limit_tripped
|
||||
def reset_pos_limit_tripped(self):
|
||||
self.is_pos_limit_tripped = False
|
||||
|
||||
# machine hard limits_tripped status
|
||||
def set_limits_tripped(self, data):
|
||||
self.is_limits_tripped = data
|
||||
if data:
|
||||
self._toggle_properties('hard_limits_state')
|
||||
def get_limits_tripped(self):
|
||||
return self.is_limits_tripped
|
||||
def reset_limits_tripped(self):
|
||||
self.is_limits_tripped = False
|
||||
|
||||
def set_follow_pin(self, data):
|
||||
self._follow_halpin = data
|
||||
if data:
|
||||
self._toggle_properties('follow_halpin')
|
||||
def get_follow_pin(self):
|
||||
return self._follow_halpin
|
||||
def reset_follow_pin(self):
|
||||
self._follow_halpin = False
|
||||
|
||||
# Non bool
|
||||
|
||||
# machine_joint_number status
|
||||
def set_joint_number(self, data):
|
||||
self.joint_number = data
|
||||
def get_joint_number(self):
|
||||
return self.joint_number
|
||||
def reset_joint_number(self):
|
||||
self.joint_number = 0
|
||||
|
||||
def set_spindle_near_percent(self, data):
|
||||
if data < 0: data = 0
|
||||
if data> 100: data = 100
|
||||
self._at_speed_percent = data/100.0
|
||||
def get_spindle_near_percent(self):
|
||||
return int(self._at_speed_percent * 100)
|
||||
def reset_spindle_near_percent(self):
|
||||
self._at_speed_percent = 0.10
|
||||
|
||||
def set_halpin_name(self, data):
|
||||
self._halpin_name = data
|
||||
def get_halpin_name(self):
|
||||
return self._halpin_name
|
||||
def reset_halpin_name(self):
|
||||
self._halpin_name = ''
|
||||
|
||||
# designer will show these properties in this order:
|
||||
# BOOL
|
||||
invert_state_status = Property(bool, get_invert_state, set_invert_state, reset_invert_state)
|
||||
is_paused_status = Property(bool, get_is_paused, set_is_paused, reset_is_paused)
|
||||
is_estopped_status = Property(bool, get_is_estopped, set_is_estopped, reset_is_estopped)
|
||||
is_on_status = Property(bool, get_is_on, set_is_on, reset_is_on)
|
||||
is_idle_status = Property(bool, get_is_idle, set_is_idle, reset_is_idle)
|
||||
is_homed_status = Property(bool, get_is_homed, set_is_homed, reset_is_homed)
|
||||
is_flood_status = Property(bool, get_is_flood, set_is_flood, reset_is_flood)
|
||||
is_mist_status = Property(bool, get_is_mist, set_is_mist, reset_is_mist)
|
||||
is_block_delete_status = Property(bool, get_is_block_delete, set_is_block_delete, reset_is_block_delete)
|
||||
is_optional_stop_status = Property(bool, get_is_optional_stop, set_is_optional_stop, reset_is_optional_stop)
|
||||
is_joint_homed_status = Property(bool, get_is_joint_homed, set_is_joint_homed, reset_is_joint_homed)
|
||||
is_limits_overridden_status = Property(bool, get_is_limits_overridden, set_is_limits_overridden,
|
||||
reset_is_limits_overridden)
|
||||
is_manual_status = Property(bool, get_is_manual, set_is_manual, reset_is_manual)
|
||||
is_mdi_status = Property(bool, get_is_mdi, set_is_mdi, reset_is_mdi)
|
||||
is_auto_status = Property(bool, get_is_auto, set_is_auto, reset_is_auto)
|
||||
is_spindle_stopped_status = Property(bool, get_is_spindle_stopped, set_is_spindle_stopped, reset_is_spindle_stopped)
|
||||
is_spindle_fwd_status = Property(bool, get_is_spindle_fwd, set_is_spindle_fwd, reset_is_spindle_fwd)
|
||||
is_spindle_rev_status = Property(bool, get_is_spindle_rev, set_is_spindle_rev, reset_is_spindle_rev)
|
||||
is_spindle_at_speed_status = Property(bool, get_is_spindle_at_speed, set_is_spindle_at_speed, reset_is_spindle_at_speed)
|
||||
neg_hard_limit_state = Property(bool, get_neg_limit_tripped, set_neg_limit_tripped, reset_neg_limit_tripped)
|
||||
pos_hard_limit_state = Property(bool, get_pos_limit_tripped, set_pos_limit_tripped, reset_pos_limit_tripped)
|
||||
hard_limits_state = Property(bool, get_limits_tripped, set_limits_tripped, reset_limits_tripped)
|
||||
follow_halpin_state = Property(bool, get_follow_pin, set_follow_pin, reset_follow_pin)
|
||||
|
||||
# NON BOOL
|
||||
joint_number_status = Property(int, get_joint_number, set_joint_number, reset_joint_number)
|
||||
spindle_near_percent_status = Property(int, get_spindle_near_percent, set_spindle_near_percent, reset_spindle_near_percent)
|
||||
halpin_name = Property(str, get_halpin_name, set_halpin_name, reset_halpin_name)
|
||||
|
||||
# boilder code
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from qtpy.QtGui import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
led = StateLED()
|
||||
led.show()
|
||||
#led.startFlashing()
|
||||
sys.exit(app.exec())
|
||||
985
lib/python/qtvcp/widgets/status_label.py
Normal file
@@ -0,0 +1,985 @@
|
||||
#!/usr/bin/env python3
|
||||
# QTVcp Widget
|
||||
#
|
||||
# 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 time
|
||||
import os
|
||||
from qtpy import QtCore
|
||||
|
||||
from qtvcp import logger
|
||||
from qtvcp.widgets.simple_widgets import ScaledLabel
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Tool
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INFO is INI file details
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
TOOL = Tool()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
|
||||
class StatusLabel(ScaledLabel, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StatusLabel, self).__init__(parent)
|
||||
self.display_units_mm = False
|
||||
self._textTemplate = '%s'
|
||||
self._alt_textTemplate = '%s'
|
||||
self._actual_RPM = 0
|
||||
self._diameter = 1
|
||||
self._index = 0
|
||||
self._tool_dia = 0
|
||||
self._tool_offset = 0
|
||||
self._state_label_list = ['Estopped','Running','Stopped','Paused','Waiting','Reading']
|
||||
self._motion_label_list = ['None','Rapid','Feed','Arc','Tool Change','Probe','Rotaty Index']
|
||||
self._halpin_name = 'remapStat.tool'
|
||||
|
||||
self.feed_override = False
|
||||
self.rapid_override = False
|
||||
self.max_velocity_override = False
|
||||
self.spindle_override = False
|
||||
self.jograte = False
|
||||
self.jograte_angular = False
|
||||
self.jogincr = False
|
||||
self.jogincr_angular = False
|
||||
self.tool_number = False
|
||||
self.current_feedrate = False
|
||||
self.current_feedunit = False
|
||||
self.requested_spindle_speed = False
|
||||
self.actual_spindle_speed = False
|
||||
self.actual_surface_speed = False
|
||||
self.user_system = False
|
||||
self.blendcode = False
|
||||
self.fcode = False
|
||||
self.gcodes = False
|
||||
self.gcode_group0 = False
|
||||
self.gcode_group1 = False
|
||||
self.gcode_group2 = False
|
||||
self.gcode_group3 = False
|
||||
self.gcode_group4 = False
|
||||
self.gcode_group5 = False
|
||||
self.gcode_group6 = False
|
||||
self.gcode_group7 = False
|
||||
self.gcode_group8 = False
|
||||
self.gcode_group10 = False
|
||||
self.gcode_group12 = False
|
||||
self.gcode_group13 = False
|
||||
self.gcode_group14 = False
|
||||
self.gcode_group15 = False
|
||||
self.mcodes = False
|
||||
self.tool_diameter = False
|
||||
self.tool_comment = False
|
||||
self.filename = False
|
||||
self.filepath = False
|
||||
self.machine_state = False
|
||||
self.motion_type = False
|
||||
self.time_stamp = False
|
||||
self.tool_offset = False
|
||||
self.gcode_selected = False
|
||||
self.halpin = False
|
||||
|
||||
def _hal_init(self):
|
||||
super(StatusLabel, self)._hal_init()
|
||||
def _f(data):
|
||||
self._set_text(data)
|
||||
|
||||
for g in ('group0','group1','group2','group3','group4','group5',
|
||||
'group6','group7','group8','group10','group12','group13','group14','group15'):
|
||||
if self['gcode_{}'.format(g)]:
|
||||
STATUS.connect('gcode-{}-changed'.format(g), lambda w, data: _f(data))
|
||||
break
|
||||
return
|
||||
|
||||
if self.feed_override:
|
||||
STATUS.connect('feed-override-changed', lambda w, data: _f(data))
|
||||
elif self.rapid_override:
|
||||
STATUS.connect('rapid-override-changed', lambda w, data: _f(data))
|
||||
elif self.max_velocity_override:
|
||||
STATUS.connect('max-velocity-override-changed', lambda w, data: self._set_max_velocity(data))
|
||||
STATUS.connect('metric-mode-changed', self._switch_max_velocity_units)
|
||||
elif self.spindle_override:
|
||||
STATUS.connect('spindle-override-changed', lambda w, data: _f(data))
|
||||
elif self.jograte:
|
||||
STATUS.connect('jograte-changed', lambda w, data: self._set_jograte(data))
|
||||
STATUS.connect('metric-mode-changed', self._switch_jog_units)
|
||||
elif self.jograte_angular:
|
||||
STATUS.connect('jograte-angular-changed', lambda w, data: _f(data))
|
||||
elif self.jogincr:
|
||||
STATUS.connect('jogincrement-changed', lambda w, data, text: _f(text))
|
||||
elif self.jogincr_angular:
|
||||
STATUS.connect('jogincrement-angular-changed', lambda w, data, text: _f(text))
|
||||
elif self.tool_number:
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w, data: _f(data))
|
||||
elif self.current_feedrate:
|
||||
STATUS.connect('current-feed-rate', self._set_feedrate_text)
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
elif self.current_feedunit:
|
||||
STATUS.connect('current-feed-rate', self._set_feedunit_text)
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('actual-spindle-speed-changed', self._set_actual_rpm)
|
||||
elif self.requested_spindle_speed:
|
||||
STATUS.connect('requested-spindle-speed-changed', lambda w, data: _f(data))
|
||||
elif self.actual_spindle_speed:
|
||||
STATUS.connect('actual-spindle-speed-changed', lambda w, data: _f(data))
|
||||
elif self.user_system:
|
||||
STATUS.connect('user-system-changed', self._set_user_system_text)
|
||||
elif self.blendcode:
|
||||
STATUS.connect('blend-code-changed', lambda w, blend, cam: self._set_blend_text(blend, cam))
|
||||
elif self.fcode:
|
||||
STATUS.connect('f-code-changed', lambda w, data: self._set_fcode_text(data))
|
||||
elif self.gcodes:
|
||||
STATUS.connect('g-code-changed', lambda w, data: _f(data))
|
||||
elif self.mcodes:
|
||||
STATUS.connect('m-code-changed', lambda w, data: _f(data))
|
||||
elif self.tool_diameter:
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data, 'diameter'))
|
||||
STATUS.connect('metric-mode-changed', lambda w, data: self._switch_tool_diam_units(data))
|
||||
elif self.tool_comment:
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._tool_file_info(data, TOOL.COMMENTS))
|
||||
elif self.actual_surface_speed:
|
||||
if INFO.MACHINE_IS_LATHE:
|
||||
STATUS.connect('current-x-rel-position', lambda w, data: self._set_work_diameter(data*2))
|
||||
else:
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._ss_tool_diam(data))
|
||||
STATUS.connect('actual-spindle-speed-changed', lambda w, data: self._ss_spindle_speed(data))
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('metric-mode-changed', lambda o, state: self._set_surface_speed())
|
||||
|
||||
elif self.filename or self.filepath:
|
||||
STATUS.connect('file-loaded', self._file_loaded)
|
||||
elif self.machine_state:
|
||||
STATUS.connect('state-estop', lambda w: self._machine_state(self._state_label_list[0]))
|
||||
STATUS.connect('interp-run', lambda w: self._machine_state(self._state_label_list[1]))
|
||||
STATUS.connect('interp-idle', lambda w: self._machine_state(self._state_label_list[2]))
|
||||
STATUS.connect('interp-paused', lambda w: self._machine_state(self._state_label_list[3]))
|
||||
STATUS.connect('interp-waiting', lambda w: self._machine_state(self._state_label_list[4]))
|
||||
STATUS.connect('interp-reading', lambda w: self._machine_state(self._state_label_list[5]))
|
||||
elif self.motion_type:
|
||||
STATUS.connect('motion-type-changed', lambda w, data: self._set_motion_type(data))
|
||||
elif self.time_stamp:
|
||||
STATUS.connect('periodic', self._set_timestamp)
|
||||
elif self.tool_offset:
|
||||
STATUS.connect('current-tool-offset', self._set_tool_offset_text)
|
||||
STATUS.connect('metric-mode-changed', lambda w, data: self._switch_tool_offsets_units(data))
|
||||
elif self.gcode_selected:
|
||||
STATUS.connect('gcode-line-selected', lambda w, line: _f(int(line)+1))
|
||||
elif self.halpin:
|
||||
STATUS.connect('periodic', lambda w: self._set_halpin_text())
|
||||
else:
|
||||
LOG.debug('{} : no option recognised'.format(self.HAL_NAME_))
|
||||
|
||||
def _set_text(self, data):
|
||||
tmpl = lambda s: str(self._textTemplate) % s
|
||||
self.setText(tmpl(data))
|
||||
def _set_alt_text(self, data):
|
||||
tmpl = lambda s: str(self._alt_textTemplate) % s
|
||||
self.setText(tmpl(data))
|
||||
|
||||
def _set_blend_text(self, blend, cam):
|
||||
tmpl = lambda s: str(self._textTemplate) % s
|
||||
alttmpl = lambda s: str(self._alt_textTemplate) % s
|
||||
self.setText(tmpl(blend) + alttmpl(cam))
|
||||
|
||||
def _set_fcode_text(self, data):
|
||||
self._set_text(data)
|
||||
|
||||
def _set_feedrate_text(self, widget, data):
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
data = INFO.convert_units(data)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
|
||||
def _set_feedunit_text(self, widget, data):
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
data = INFO.convert_units(data)
|
||||
try:
|
||||
data = abs(data/self._actual_RPM)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
except:
|
||||
self.setText('')
|
||||
return
|
||||
|
||||
def _set_actual_rpm(self, w, rpm):
|
||||
self._actual_RPM = rpm
|
||||
|
||||
def _set_user_system_text(self, widgets, data):
|
||||
convert = { 1:"54", 2:"55", 3:"56", 4:"57", 5:"58", 6:"59", 7:"59.1", 8:"59.2", 9:"59.3"}
|
||||
self._set_text(convert[int(data)])
|
||||
|
||||
def _set_machine_units(self, u, c):
|
||||
self.machine_units_mm = u
|
||||
self.unit_convert = c
|
||||
|
||||
def _switch_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
|
||||
def _switch_jog_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
self._set_jograte(STATUS.get_jograte())
|
||||
|
||||
def _switch_tool_diam_units(self, units):
|
||||
self.display_units_mm = units
|
||||
data = self.conversion(self._tool_dia)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
|
||||
def _switch_tool_offsets_units(self, units):
|
||||
self.display_units_mm = units
|
||||
data = self.conversion(self._tool_offset)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
|
||||
def _switch_max_velocity_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
self._set_max_velocity(STATUS.get_max_velocity())
|
||||
|
||||
def _tool_info(self, data, field):
|
||||
if data.id != -1:
|
||||
if field == 'diameter':
|
||||
data = self.conversion(data.diameter)
|
||||
self._tool_dia = data
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
return
|
||||
self._set_text(0)
|
||||
|
||||
# [0] = cell toggle
|
||||
# [1] = tool number
|
||||
# [2] = pocket number
|
||||
# [3] = X offset
|
||||
# [4] = Y offset
|
||||
# [5] = Z offset
|
||||
# [6] = A offset
|
||||
# [7] = B offset
|
||||
# [8] = C offset
|
||||
# [9] = U offset
|
||||
# [10] = V offset
|
||||
# [11] = W offset
|
||||
# [12] = tool diameter
|
||||
# [13] = frontangle
|
||||
# [14] = backangle
|
||||
# [15] = tool orientation
|
||||
# [16] = tool comment
|
||||
def _tool_file_info(self, tool_entry, index):
|
||||
toolnum = tool_entry[0]
|
||||
tool_table_line = TOOL.GET_TOOL_INFO(toolnum)
|
||||
try:
|
||||
self._set_text(str(tool_table_line[index]))
|
||||
except:
|
||||
LOG.warning('Problem with tool file info')
|
||||
self._set_text('')
|
||||
|
||||
def _set_tool_offset_text(self, w, offsets):
|
||||
data = self.conversion(offsets[self._index])
|
||||
self._tool_offset = offsets[self._index]
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(data)
|
||||
else:
|
||||
self._set_text(data)
|
||||
|
||||
def _ss_tool_diam(self, data):
|
||||
if data.id != -1:
|
||||
self._diameter = data.diameter
|
||||
else:
|
||||
self._diameter = 1
|
||||
self. _set_surface_speed()
|
||||
def _set_work_diameter(self, dia):
|
||||
self._diameter = dia
|
||||
self._set_surface_speed()
|
||||
def _ss_spindle_speed(self, rpm):
|
||||
self._actual_RPM = rpm
|
||||
self._set_surface_speed()
|
||||
def _set_surface_speed(self):
|
||||
diam = self.conversion(self._diameter)
|
||||
circ = abs(3.14 * self._actual_RPM * diam)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(circ/1000) # meters per minute
|
||||
else:
|
||||
self._set_text(circ/12) # feet per minute
|
||||
|
||||
# This does the conversion units
|
||||
# data must always be in machine units
|
||||
def conversion(self, data):
|
||||
if self.display_units_mm :
|
||||
return INFO.convert_machine_to_metric(data)
|
||||
else:
|
||||
return INFO.convert_machine_to_imperial(data)
|
||||
|
||||
def _file_loaded(self, w, name):
|
||||
if self.filename:
|
||||
name = os.path.basename(name)
|
||||
self._set_text(name)
|
||||
|
||||
def _machine_state(self, text):
|
||||
self._set_text(text)
|
||||
|
||||
def _set_timestamp(self, w):
|
||||
self.setText(time.strftime(self._textTemplate))
|
||||
|
||||
def _set_jograte(self, data):
|
||||
rate = self.conversion(data)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(rate)
|
||||
else:
|
||||
self._set_text(rate)
|
||||
|
||||
def _set_max_velocity(self, data):
|
||||
rate = self.conversion(data)
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(rate)
|
||||
else:
|
||||
self._set_text(rate)
|
||||
|
||||
def _set_halpin_text(self):
|
||||
try:
|
||||
rate = self.HAL_GCOMP_.hal.get_value(self._halpin_name)
|
||||
except Exception as e:
|
||||
return
|
||||
if self.display_units_mm:
|
||||
self._set_alt_text(rate)
|
||||
else:
|
||||
self._set_text(rate)
|
||||
|
||||
def _set_motion_type(self, data):
|
||||
self._set_text(self._motion_label_list[data])
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('feed_override', 'rapid_override', 'spindle_override', 'jograte',
|
||||
'jograte_angular', 'jogincr', 'jogincr_angular', 'tool_number',
|
||||
'current_feedrate', 'current_FPU',
|
||||
'requested_spindle_speed', 'actual_spindle_speed',
|
||||
'user_system', 'gcodes', 'mcodes', 'tool_diameter',
|
||||
'tool_comment', 'actual_surface_speed', 'filename', 'filepath',
|
||||
'machine_state', 'motion_type', 'time_stamp', 'max_velocity_override', 'tool_offset',
|
||||
'gcode_selected', 'fcode', 'blendcode', 'halpin')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i + '_status'] = False
|
||||
|
||||
def set_textTemplate(self, data):
|
||||
self._textTemplate = data
|
||||
try:
|
||||
self._set_text(100.0)
|
||||
except ValueError:
|
||||
try:
|
||||
self.setText(time.strftime(self._textTemplate))
|
||||
except:
|
||||
raise
|
||||
except TypeError:
|
||||
try:
|
||||
self.setText(data)
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
LOG.error("textTemplate: {}, Data: {}".format(self._textTemplate, data), exc_info=e)
|
||||
self.setText('Error')
|
||||
def get_textTemplate(self):
|
||||
return self._textTemplate
|
||||
def reset_textTemplate(self):
|
||||
self._textTemplate = '%s'
|
||||
|
||||
def set_alt_textTemplate(self, data):
|
||||
self._alt_textTemplate = data
|
||||
try:
|
||||
self._set_text(200.0)
|
||||
except TypeError:
|
||||
try:
|
||||
self.setText(data)
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
LOG.error("altTextTemplate: {}, Data: {}".format(self._textTemplate, data), exc_info=e)
|
||||
self.setText('Error 2')
|
||||
def get_alt_textTemplate(self):
|
||||
return self._alt_textTemplate
|
||||
def reset_alt_textTemplate(self):
|
||||
self._alt_textTemplate = '%s'
|
||||
|
||||
# index for tool offset
|
||||
def set_index(self, data):
|
||||
if data < 0: data = 0
|
||||
self._index = data
|
||||
def get_index(self):
|
||||
return self._index
|
||||
def reset_index(self):
|
||||
self._index = 0
|
||||
|
||||
|
||||
# feed override status
|
||||
def set_feed_override(self, data):
|
||||
self.feed_override = data
|
||||
if data:
|
||||
self._toggle_properties('feed_override')
|
||||
def get_feed_override(self):
|
||||
return self.feed_override
|
||||
def reset_feed_override(self):
|
||||
self.feed_override = False
|
||||
|
||||
# rapid override status
|
||||
def set_rapid_override(self, data):
|
||||
self.rapid_override = data
|
||||
if data:
|
||||
self._toggle_properties('rapid_override')
|
||||
def get_rapid_override(self):
|
||||
return self.rapid_override
|
||||
def reset_rapid_override(self):
|
||||
self.rapid_override = False
|
||||
|
||||
# max_velocity override status
|
||||
def set_max_velocity_override(self, data):
|
||||
self.max_velocity_override = data
|
||||
if data:
|
||||
self._toggle_properties('max_velocity_override')
|
||||
def get_max_velocity_override(self):
|
||||
return self.max_velocity_override
|
||||
def reset_max_velocity_override(self):
|
||||
self.max_velocity_override = False
|
||||
|
||||
# spindle override status
|
||||
def set_spindle_override(self, data):
|
||||
self.spindle_override = data
|
||||
if data:
|
||||
self._toggle_properties('spindle_override')
|
||||
def get_spindle_override(self):
|
||||
return self.spindle_override
|
||||
def reset_spindle_override(self):
|
||||
self.spindle_override = False
|
||||
|
||||
# jograte status
|
||||
def set_jograte(self, data):
|
||||
self.jograte = data
|
||||
if data:
|
||||
self._toggle_properties('jograte')
|
||||
def get_jograte(self):
|
||||
return self.jograte
|
||||
def reset_jograte(self):
|
||||
self.jograte = False
|
||||
|
||||
# jograte_angular status
|
||||
def set_jograte_angular(self, data):
|
||||
self.jograte_angular = data
|
||||
if data:
|
||||
self._toggle_properties('jograte_angular')
|
||||
def get_jograte_angular(self):
|
||||
return self.jograte_angular
|
||||
def reset_jograte_angular(self):
|
||||
self.jograte_angular = False
|
||||
|
||||
# jogincr status
|
||||
def set_jogincr(self, data):
|
||||
self.jogincr = data
|
||||
if data:
|
||||
self._toggle_properties('jogincr')
|
||||
def get_jogincr(self):
|
||||
return self.jogincr
|
||||
def reset_jogincr(self):
|
||||
self.jogincr = False
|
||||
|
||||
# jogincr_angular status
|
||||
def set_jogincr_angular(self, data):
|
||||
self.jogincr_angular = data
|
||||
if data:
|
||||
self._toggle_properties('jogincr_angular')
|
||||
def get_jogincr_angular(self):
|
||||
return self.jogincr_angular
|
||||
def reset_jogincr_angular(self):
|
||||
self.jogincr_angular = False
|
||||
|
||||
# tool number status
|
||||
def set_tool_number(self, data):
|
||||
self.tool_number = data
|
||||
if data:
|
||||
self._toggle_properties('tool_number')
|
||||
def get_tool_number(self):
|
||||
return self.tool_number
|
||||
def reset_tool_number(self):
|
||||
self.tool_number = False
|
||||
|
||||
# current feedrate status
|
||||
def set_current_feedrate(self, data):
|
||||
self.current_feedrate = data
|
||||
if data:
|
||||
self._toggle_properties('current_feedrate')
|
||||
def get_current_feedrate(self):
|
||||
return self.current_feedrate
|
||||
def reset_current_feedrate(self):
|
||||
self.current_feedrate = False
|
||||
|
||||
# current feedunit status
|
||||
def set_current_feedunit(self, data):
|
||||
self.current_feedunit = data
|
||||
if data:
|
||||
self._toggle_properties('current_FPU')
|
||||
def get_current_feedunit(self):
|
||||
return self.current_feedunit
|
||||
def reset_current_feedunit(self):
|
||||
self.current_feedunit = False
|
||||
|
||||
# spindle speed status
|
||||
def set_requested_spindle_speed(self, data):
|
||||
self.requested_spindle_speed = data
|
||||
if data:
|
||||
self._toggle_properties('requested_spindle_speed')
|
||||
def get_requested_spindle_speed(self):
|
||||
return self.requested_spindle_speed
|
||||
def reset_requested_spindle_speed(self):
|
||||
self.requested_spindle_speed = False
|
||||
|
||||
# spindle speed status
|
||||
def set_actual_spindle_speed(self, data):
|
||||
self.actual_spindle_speed = data
|
||||
if data:
|
||||
self._toggle_properties('actual_spindle_speed')
|
||||
def get_actual_spindle_speed(self):
|
||||
return self.actual_spindle_speed
|
||||
def reset_actual_spindle_speed(self):
|
||||
self.actual_spindle_speed = False
|
||||
|
||||
# user_system status
|
||||
def set_user_system(self, data):
|
||||
self.user_system = data
|
||||
if data:
|
||||
self._toggle_properties('user_system')
|
||||
def get_user_system(self):
|
||||
return self.user_system
|
||||
def reset_user_system(self):
|
||||
self.user_system = False
|
||||
|
||||
# fcode status
|
||||
def set_fcode(self, data):
|
||||
self.fcode = data
|
||||
if data:
|
||||
self._toggle_properties('fcode')
|
||||
def get_fcode(self):
|
||||
return self.fcode
|
||||
def reset_fcode(self):
|
||||
self.fcode = False
|
||||
|
||||
# blendcode status
|
||||
def set_blendcode(self, data):
|
||||
self.blendcode = data
|
||||
if data:
|
||||
self._toggle_properties('blendcode')
|
||||
def get_blendcode(self):
|
||||
return self.blendcode
|
||||
def reset_blendcode(self):
|
||||
self.blendcode = False
|
||||
|
||||
# gcodes status
|
||||
def set_gcodes(self, data):
|
||||
self.gcodes = data
|
||||
if data:
|
||||
self._toggle_properties('gcodes')
|
||||
def get_gcodes(self):
|
||||
return self.gcodes
|
||||
def reset_gcodes(self):
|
||||
self.gcodes = False
|
||||
|
||||
# gcode_group0 status
|
||||
def set_gcode_group0(self, data):
|
||||
self.gcode_group0 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group0')
|
||||
def get_gcode_group0(self):
|
||||
return self.gcode_group0
|
||||
def reset_gcode_group0(self):
|
||||
self.gcode_group0 = False
|
||||
|
||||
# gcode_group1 status
|
||||
def set_gcode_group1(self, data):
|
||||
self.gcode_group1 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group1')
|
||||
def get_gcode_group1(self):
|
||||
return self.gcode_group1
|
||||
def reset_gcode_group1(self):
|
||||
self.gcode_group1 = False
|
||||
|
||||
# gcode_group2 status
|
||||
def set_gcode_group2(self, data):
|
||||
self.gcode_group2 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group2')
|
||||
def get_gcode_group2(self):
|
||||
return self.gcode_group2
|
||||
def reset_gcode_group2(self):
|
||||
self.gcode_group2 = False
|
||||
|
||||
# gcode_group3 status
|
||||
def set_gcode_group3(self, data):
|
||||
self.gcode_group3 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group3')
|
||||
def get_gcode_group3(self):
|
||||
return self.gcode_group3
|
||||
def reset_gcode_group3(self):
|
||||
self.gcode_group3 = False
|
||||
|
||||
# gcode_group4 status
|
||||
def set_gcode_group4(self, data):
|
||||
self.gcode_group4 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group4')
|
||||
def get_gcode_group4(self):
|
||||
return self.gcode_group4
|
||||
def reset_gcode_group4(self):
|
||||
self.gcode_group4 = False
|
||||
|
||||
# gcode_group5 status
|
||||
def set_gcode_group5(self, data):
|
||||
self.gcode_group5 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group5')
|
||||
def get_gcode_group5(self):
|
||||
return self.gcode_group5
|
||||
def reset_gcode_group5(self):
|
||||
self.gcode_group5 = False
|
||||
|
||||
# gcode_group6 status
|
||||
def set_gcode_group6(self, data):
|
||||
self.gcode_group6 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group6')
|
||||
def get_gcode_group6(self):
|
||||
return self.gcode_group6
|
||||
def reset_gcode_group6(self):
|
||||
self.gcode_group6 = False
|
||||
|
||||
# gcode_group7 status
|
||||
def set_gcode_group7(self, data):
|
||||
self.gcode_group7 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group7')
|
||||
def get_gcode_group7(self):
|
||||
return self.gcode_group7
|
||||
def reset_gcode_group7(self):
|
||||
self.gcode_group7 = False
|
||||
|
||||
# gcode_group8 status
|
||||
def set_gcode_group8(self, data):
|
||||
self.gcode_group8 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group8')
|
||||
def get_gcode_group8(self):
|
||||
return self.gcode_group8
|
||||
def reset_gcode_group8(self):
|
||||
self.gcode_group8 = False
|
||||
|
||||
# gcode_group10 status
|
||||
def set_gcode_group10(self, data):
|
||||
self.gcode_group10 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group10')
|
||||
def get_gcode_group10(self):
|
||||
return self.gcode_group10
|
||||
def reset_gcode_group10(self):
|
||||
self.gcode_group10 = False
|
||||
|
||||
# gcode_group12 status
|
||||
def set_gcode_group12(self, data):
|
||||
self.gcode_group12 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group12')
|
||||
def get_gcode_group12(self):
|
||||
return self.gcode_group12
|
||||
def reset_gcode_group12(self):
|
||||
self.gcode_group12 = False
|
||||
|
||||
# gcode_group13 status
|
||||
def set_gcode_group13(self, data):
|
||||
self.gcode_group13 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group13')
|
||||
def get_gcode_group13(self):
|
||||
return self.gcode_group13
|
||||
def reset_gcode_group13(self):
|
||||
self.gcode_group13 = False
|
||||
|
||||
# gcode_group14 status
|
||||
def set_gcode_group14(self, data):
|
||||
self.gcode_group14 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group14')
|
||||
def get_gcode_group14(self):
|
||||
return self.gcode_group14
|
||||
def reset_gcode_group14(self):
|
||||
self.gcode_group14 = False
|
||||
|
||||
# gcode_group15 status
|
||||
def set_gcode_group15(self, data):
|
||||
self.gcode_group15 = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_group15')
|
||||
def get_gcode_group15(self):
|
||||
return self.gcode_group15
|
||||
def reset_gcode_group15(self):
|
||||
self.gcode_group15 = False
|
||||
|
||||
# mcodes status
|
||||
def set_mcodes(self, data):
|
||||
self.mcodes = data
|
||||
if data:
|
||||
self._toggle_properties('mcodes')
|
||||
def get_mcodes(self):
|
||||
return self.mcodes
|
||||
def reset_mcodes(self):
|
||||
self.mcodes = False
|
||||
|
||||
# tool_diameter status
|
||||
def set_tool_diameter(self, data):
|
||||
self.tool_diameter = data
|
||||
if data:
|
||||
self._toggle_properties('tool_diameter')
|
||||
def get_tool_diameter(self):
|
||||
return self.tool_diameter
|
||||
def reset_tool_diameter(self):
|
||||
self.tool_diameter = False
|
||||
|
||||
# tool_comment status
|
||||
def set_tool_comment(self, data):
|
||||
self.tool_comment = data
|
||||
if data:
|
||||
self._toggle_properties('tool_comment')
|
||||
def get_tool_comment(self):
|
||||
return self.tool_comment
|
||||
def reset_tool_comment(self):
|
||||
self.tool_comment = False
|
||||
|
||||
# actual_surface_speed status
|
||||
def set_actual_surface_speed(self, data):
|
||||
self.actual_surface_speed = data
|
||||
if data:
|
||||
self._toggle_properties('actual_surface_speed')
|
||||
def get_actual_surface_speed(self):
|
||||
return self.actual_surface_speed
|
||||
def reset_actual_surface_speed(self):
|
||||
self.actual_surface_speed = False
|
||||
|
||||
# filename status
|
||||
def set_filename(self, data):
|
||||
self.filename = data
|
||||
if data:
|
||||
self._toggle_properties('filename')
|
||||
def get_filename(self):
|
||||
return self.filename
|
||||
def reset_filename(self):
|
||||
self.filename = False
|
||||
|
||||
# filepath status
|
||||
def set_filepath(self, data):
|
||||
self.filepath = data
|
||||
if data:
|
||||
self._toggle_properties('filepath')
|
||||
def get_filepath(self):
|
||||
return self.filepath
|
||||
def reset_filepath(self):
|
||||
self.filepath = False
|
||||
|
||||
# machine_state status
|
||||
def set_machine_state(self, data):
|
||||
self.machine_state = data
|
||||
if data:
|
||||
self._toggle_properties('machine_state')
|
||||
def get_machine_state(self):
|
||||
return self.machine_state
|
||||
def reset_machine_state(self):
|
||||
self.machine_state = False
|
||||
|
||||
# motion_type status
|
||||
def set_motion_type(self, data):
|
||||
self.motion_type = data
|
||||
if data:
|
||||
self._toggle_properties('motion_type')
|
||||
def get_motion_type(self):
|
||||
return self.motion_type
|
||||
def reset_motion_type(self):
|
||||
self.motion_type = False
|
||||
|
||||
# time_stamp status
|
||||
def set_time_stamp(self, data):
|
||||
self.time_stamp = data
|
||||
if data:
|
||||
self._toggle_properties('time_stamp')
|
||||
def get_time_stamp(self):
|
||||
return self.time_stamp
|
||||
def reset_time_stamp(self):
|
||||
self.time_stamp = False
|
||||
|
||||
# tool_offset status
|
||||
def set_tool_offset(self, data):
|
||||
self.tool_offset = data
|
||||
if data:
|
||||
self._toggle_properties('tool_offset')
|
||||
def get_tool_offset(self):
|
||||
return self.tool_offset
|
||||
def reset_tool_offset(self):
|
||||
self.tool_offset = False
|
||||
|
||||
def set_state_label_l(self, data):
|
||||
self._state_label_list = data
|
||||
def get_state_label_l(self):
|
||||
return self._state_label_list
|
||||
def reset_state_label_l(self):
|
||||
self._state_label_list = ['Estopped','Running','Stopped','Paused','Waiting','Reading']
|
||||
|
||||
def set_motion_label_l(self, data):
|
||||
self._motion_label_list = data
|
||||
def get_motion_label_l(self):
|
||||
return self._motion_label_list
|
||||
def reset_motion_label_l(self):
|
||||
self._motion_label_list = ['None','Rapid','Feed','Arc','Tool Change','Probe','Rotaty Index']
|
||||
|
||||
# gcode line selected
|
||||
def set_gcode_selected(self, data):
|
||||
self.gcode_selected = data
|
||||
if data:
|
||||
self._toggle_properties('gcode_selected')
|
||||
def get_gcode_selected(self):
|
||||
return self.gcode_selected
|
||||
def reset_gcode_selected(self):
|
||||
self.gcode_selected = False
|
||||
|
||||
# gcode line selected
|
||||
def set_halpin(self, data):
|
||||
self.halpin = data
|
||||
if data:
|
||||
self._toggle_properties('halpin')
|
||||
def get_halpin(self):
|
||||
return self.halpin
|
||||
def reset_halpin(self):
|
||||
self.halpin = False
|
||||
|
||||
def set_halpin_name(self, data):
|
||||
self._halpin_name = data
|
||||
def get_halpin_name(self):
|
||||
return self._halpin_name
|
||||
def reset_halpin_name(self):
|
||||
self._halpin_name = ''
|
||||
|
||||
textTemplate = QtCore.Property(str, get_textTemplate, set_textTemplate, reset_textTemplate)
|
||||
alt_textTemplate = QtCore.Property(str, get_alt_textTemplate, set_alt_textTemplate, reset_alt_textTemplate)
|
||||
index_number = QtCore.Property(int, get_index, set_index, reset_index)
|
||||
|
||||
feed_override_status = QtCore.Property(bool, get_feed_override, set_feed_override, reset_feed_override)
|
||||
rapid_override_status = QtCore.Property(bool, get_rapid_override, set_rapid_override, reset_rapid_override)
|
||||
max_velocity_override_status = QtCore.Property(bool, get_max_velocity_override, set_max_velocity_override, reset_max_velocity_override)
|
||||
spindle_override_status = QtCore.Property(bool, get_spindle_override, set_spindle_override,
|
||||
reset_spindle_override)
|
||||
jograte_status = QtCore.Property(bool, get_jograte, set_jograte, reset_jograte)
|
||||
jograte_angular_status = QtCore.Property(bool, get_jograte_angular, set_jograte_angular, reset_jograte_angular)
|
||||
jogincr_status = QtCore.Property(bool, get_jogincr, set_jogincr, reset_jogincr)
|
||||
jogincr_angular_status = QtCore.Property(bool, get_jogincr_angular, set_jogincr_angular, reset_jogincr_angular)
|
||||
current_feedrate_status = QtCore.Property(bool, get_current_feedrate, set_current_feedrate,
|
||||
reset_current_feedrate)
|
||||
current_FPU_status = QtCore.Property(bool, get_current_feedunit, set_current_feedunit, reset_current_feedunit)
|
||||
requested_spindle_speed_status = QtCore.Property(bool, get_requested_spindle_speed,
|
||||
set_requested_spindle_speed, reset_requested_spindle_speed)
|
||||
actual_spindle_speed_status = QtCore.Property(bool, get_actual_spindle_speed, set_actual_spindle_speed,
|
||||
reset_actual_spindle_speed)
|
||||
user_system_status = QtCore.Property(bool, get_user_system, set_user_system, reset_user_system)
|
||||
blendcode_status = QtCore.Property(bool, get_blendcode, set_blendcode, reset_blendcode)
|
||||
fcode_status = QtCore.Property(bool, get_fcode, set_fcode, reset_fcode)
|
||||
gcodes_status = QtCore.Property(bool, get_gcodes, set_gcodes, reset_gcodes)
|
||||
gcode_group0_status = QtCore.Property(bool, get_gcode_group0,
|
||||
set_gcode_group0, reset_gcode_group0)
|
||||
gcode_group1_status = QtCore.Property(bool, get_gcode_group1,
|
||||
set_gcode_group1, reset_gcode_group1)
|
||||
gcode_group2_status = QtCore.Property(bool, get_gcode_group2,
|
||||
set_gcode_group2, reset_gcode_group2)
|
||||
gcode_group3_status = QtCore.Property(bool, get_gcode_group3,
|
||||
set_gcode_group3, reset_gcode_group3)
|
||||
gcode_group4_status = QtCore.Property(bool, get_gcode_group4,
|
||||
set_gcode_group4, reset_gcode_group4)
|
||||
gcode_group5_status = QtCore.Property(bool, get_gcode_group5,
|
||||
set_gcode_group5, reset_gcode_group5)
|
||||
gcode_group6_status = QtCore.Property(bool, get_gcode_group6,
|
||||
set_gcode_group6, reset_gcode_group6)
|
||||
gcode_group7_status = QtCore.Property(bool, get_gcode_group7,
|
||||
set_gcode_group7, reset_gcode_group7)
|
||||
gcode_group8_status = QtCore.Property(bool, get_gcode_group8,
|
||||
set_gcode_group8, reset_gcode_group8)
|
||||
gcode_group10_status = QtCore.Property(bool, get_gcode_group10,
|
||||
set_gcode_group10, reset_gcode_group10)
|
||||
gcode_group12_status = QtCore.Property(bool, get_gcode_group12,
|
||||
set_gcode_group12, reset_gcode_group12)
|
||||
gcode_group13_status = QtCore.Property(bool, get_gcode_group13,
|
||||
set_gcode_group13, reset_gcode_group13)
|
||||
gcode_group14_status = QtCore.Property(bool, get_gcode_group14,
|
||||
set_gcode_group14, reset_gcode_group14)
|
||||
gcode_group15_status = QtCore.Property(bool, get_gcode_group15,
|
||||
set_gcode_group15, reset_gcode_group15)
|
||||
mcodes_status = QtCore.Property(bool, get_mcodes, set_mcodes, reset_mcodes)
|
||||
tool_diameter_status = QtCore.Property(bool, get_tool_diameter, set_tool_diameter, reset_tool_diameter)
|
||||
tool_comment_status = QtCore.Property(bool, get_tool_comment, set_tool_comment, reset_tool_comment)
|
||||
tool_number_status = QtCore.Property(bool, get_tool_number, set_tool_number, reset_tool_number)
|
||||
tool_offset_status = QtCore.Property(bool, get_tool_offset, set_tool_offset, reset_tool_offset)
|
||||
gcode_selected_status = QtCore.Property(bool, get_gcode_selected, set_gcode_selected, reset_gcode_selected)
|
||||
actual_surface_speed_status = QtCore.Property(bool, get_actual_surface_speed, set_actual_surface_speed,
|
||||
reset_actual_surface_speed)
|
||||
filename_status = QtCore.Property(bool, get_filename, set_filename,
|
||||
reset_filename)
|
||||
filepath_status = QtCore.Property(bool, get_filepath, set_filepath,
|
||||
reset_filepath)
|
||||
machine_state_status = QtCore.Property(bool, get_machine_state, set_machine_state,
|
||||
reset_machine_state)
|
||||
motion_type_status = QtCore.Property(bool, get_motion_type, set_motion_type,
|
||||
reset_motion_type)
|
||||
time_stamp_status = QtCore.Property(bool, get_time_stamp, set_time_stamp,
|
||||
reset_time_stamp)
|
||||
halpin_status = QtCore.Property(bool, get_halpin, set_halpin, reset_halpin)
|
||||
state_label_list = QtCore.Property('QStringList', get_state_label_l, set_state_label_l, reset_state_label_l)
|
||||
motion_type_list = QtCore.Property('QStringList', get_motion_label_l, set_motion_label_l, reset_motion_label_l)
|
||||
halpin_name = QtCore.Property(str, get_halpin_name, set_halpin_name, reset_halpin_name)
|
||||
# boilder code
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
label = StatusLabel()
|
||||
label.setProperty('motion_type_status',True)
|
||||
label._hal_init()
|
||||
label.show()
|
||||
sys.exit(app.exec())
|
||||
296
lib/python/qtvcp/widgets/status_slider.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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 hal
|
||||
|
||||
from qtpy import QtWidgets
|
||||
from qtpy.QtCore import Property, Signal
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# ACTION gives commands to linuxcnc
|
||||
# INFO is INI file details
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
# Based on https://stackoverflow.com/questions/42820380/use-float-for-qslider
|
||||
class DoubleSlider(QtWidgets.QSlider):
|
||||
|
||||
# create our our signal that we can connect to if necessary
|
||||
doubleValueChanged = Signal(float)
|
||||
|
||||
def __init__(self, *args, **kargs):
|
||||
super(DoubleSlider, self).__init__( *args, **kargs)
|
||||
self._multi = 1 ** 2 # arbitrarily set
|
||||
|
||||
# not needed at this time
|
||||
self.valueChanged.connect(self.emitDoubleValueChanged)
|
||||
|
||||
def emitDoubleValueChanged(self):
|
||||
value = float(super(DoubleSlider, self).value())/self._multi
|
||||
self.doubleValueChanged.emit(value)
|
||||
|
||||
def value(self):
|
||||
return float(super(DoubleSlider, self).value()) / self._multi
|
||||
|
||||
def setMinimum(self, value):
|
||||
return super(DoubleSlider, self).setMinimum(int(value * self._multi))
|
||||
|
||||
def setMaximum(self, value):
|
||||
return super(DoubleSlider, self).setMaximum(int(value * self._multi))
|
||||
|
||||
def setSingleStep(self, value):
|
||||
return super(DoubleSlider, self).setSingleStep(value * self._multi)
|
||||
|
||||
def singleStep(self):
|
||||
return float(super(DoubleSlider, self).singleStep()) / self._multi
|
||||
|
||||
def setValue(self, value):
|
||||
super(DoubleSlider, self).setValue(int(value * self._multi))
|
||||
|
||||
class StatusSlider(DoubleSlider, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StatusSlider, self).__init__(parent)
|
||||
self._block_signal = False
|
||||
self._halpin_option = True
|
||||
self.rapid = True
|
||||
self.feed = False
|
||||
self.spindle = False
|
||||
self.jograte = False
|
||||
self.jograte_angular = False
|
||||
self.max_velocity = False
|
||||
|
||||
# for syslesheet dynamic property
|
||||
self._alertState = ''
|
||||
self._alertOver = 100.0
|
||||
self._alertUnder = 50.0
|
||||
|
||||
def _hal_init(self):
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop-reset', lambda w: self.setEnabled(True))
|
||||
if self.rapid:
|
||||
STATUS.connect('rapid-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(100)
|
||||
elif self.feed:
|
||||
STATUS.connect('feed-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_FEED_OVERRIDE))
|
||||
elif self.spindle:
|
||||
STATUS.connect('spindle-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_SPINDLE_OVERRIDE))
|
||||
self.setMinimum(int(INFO.MIN_SPINDLE_OVERRIDE))
|
||||
elif self.jograte:
|
||||
STATUS.connect('jograte-changed', lambda w, data: self.setValue(data))
|
||||
self.setMinimum(int(INFO.MIN_LINEAR_JOG_VEL))
|
||||
self.setMaximum(int(INFO.MAX_LINEAR_JOG_VEL))
|
||||
elif self.jograte_angular:
|
||||
STATUS.connect('jograte-angular-changed', lambda w, data: self.setValue(data))
|
||||
self.setMinimum(int(INFO.MIN_ANGULAR_JOG_VEL))
|
||||
self.setMaximum(int(INFO.MAX_ANGULAR_JOG_VEL))
|
||||
elif self.max_velocity:
|
||||
STATUS.connect('max-velocity-override-changed', lambda w, data: self.setValue(data))
|
||||
self.setMaximum(int(INFO.MAX_TRAJ_VELOCITY))
|
||||
else:
|
||||
LOG.error('{} : no option recognised'.format(self.HAL_NAME_))
|
||||
|
||||
if self._halpin_option:
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(str(pname), hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
|
||||
# connect a signal and callback function to the button
|
||||
self.doubleValueChanged.connect(self._action)
|
||||
# If the widget uses dynamic properties in stylesheet...
|
||||
self._style_polish(state= self.get_alert_cmd(self.value()))
|
||||
|
||||
# catch any programmed settings and update HAL pin
|
||||
def setValue(self, v):
|
||||
super(StatusSlider, self).setValue(v)
|
||||
if self._halpin_option:
|
||||
self.hal_pin.set(v)
|
||||
|
||||
def _action(self, value):
|
||||
self._style_polish(state= self.get_alert_cmd(value))
|
||||
if self._halpin_option:
|
||||
self.hal_pin.set(value)
|
||||
if self.rapid:
|
||||
ACTION.SET_RAPID_RATE(value)
|
||||
elif self.feed:
|
||||
ACTION.SET_FEED_RATE(value)
|
||||
elif self.spindle:
|
||||
ACTION.SET_SPINDLE_RATE(value)
|
||||
elif self.jograte:
|
||||
ACTION.SET_JOG_RATE(value)
|
||||
elif self.jograte_angular:
|
||||
ACTION.SET_JOG_RATE_ANGULAR(value)
|
||||
elif self.max_velocity:
|
||||
ACTION.SET_MAX_VELOCITY_RATE(value)
|
||||
else:
|
||||
LOG.error('{} : no action recognised'.format(self.HAL_NAME_))
|
||||
|
||||
# convert numeric value to string command
|
||||
def get_alert_cmd(self, value):
|
||||
if value > self._alertOver:
|
||||
return 'over'
|
||||
elif value < self._alertUnder:
|
||||
return 'under'
|
||||
else:
|
||||
return'normal'
|
||||
|
||||
# polish widget so stylesheet sees the property change
|
||||
# some stylesheets color the widget based on the arbitrary hi/lo range
|
||||
def _style_polish(self, prop = 'alertState',state = 'normal'):
|
||||
if self._alertState != state:
|
||||
self.setProperty(prop, state)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
#########################################################################
|
||||
# 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
|
||||
#
|
||||
# _toggle_properties makes it so we can only select one option
|
||||
########################################################################
|
||||
|
||||
def _toggle_properties(self, picked):
|
||||
data = ('rapid', 'feed', 'spindle', 'jograte', 'jograte_angular',
|
||||
'max-velocity')
|
||||
|
||||
for i in data:
|
||||
if not i == picked:
|
||||
self[i+'_rate'] = False
|
||||
|
||||
def setrapid(self, data):
|
||||
self.rapid = data
|
||||
if data:
|
||||
self._toggle_properties('rapid')
|
||||
def getrapid(self):
|
||||
return self.rapid
|
||||
def resetrapid(self):
|
||||
self.rapid = False
|
||||
|
||||
def setfeed(self, data):
|
||||
self.feed = data
|
||||
if data:
|
||||
self._toggle_properties('feed')
|
||||
def getfeed(self):
|
||||
return self.feed
|
||||
def resetfeed(self):
|
||||
self.feed = False
|
||||
|
||||
def setspindle(self, data):
|
||||
self.spindle = data
|
||||
if data:
|
||||
self._toggle_properties('spindle')
|
||||
def getspindle(self):
|
||||
return self.spindle
|
||||
def resetspindle(self):
|
||||
self.spindle = False
|
||||
|
||||
def setjograte(self, data):
|
||||
self.jograte = data
|
||||
if data:
|
||||
self._toggle_properties('jograte')
|
||||
def getjograte(self):
|
||||
return self.jograte
|
||||
def resetjograte(self):
|
||||
self.jograte = False
|
||||
|
||||
def setjograte_angular(self, data):
|
||||
self.jograte_angular = data
|
||||
if data:
|
||||
self._toggle_properties('jograte_angular')
|
||||
def getjograte_angular(self):
|
||||
return self.jograte_angular
|
||||
def resetjograte_angular(self):
|
||||
self.jograte_angular = False
|
||||
|
||||
def setmax_velocity(self, data):
|
||||
self.max_velocity = data
|
||||
if data:
|
||||
self._toggle_properties('max_velocity')
|
||||
def getmax_velocity(self):
|
||||
return self.max_velocity
|
||||
def resetmax_velocity(self):
|
||||
self.max_velocity = False
|
||||
|
||||
def set_halpin_option(self, value):
|
||||
self._halpin_option = value
|
||||
def get_halpin_option(self):
|
||||
return self._halpin_option
|
||||
def reset_halpin_option(self):
|
||||
self._halpin_option = True
|
||||
|
||||
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 = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
halpin_option = Property(bool, get_halpin_option, set_halpin_option, reset_halpin_option)
|
||||
rapid_rate = Property(bool, getrapid, setrapid, resetrapid)
|
||||
feed_rate = Property(bool, getfeed, setfeed, resetfeed)
|
||||
spindle_rate = Property(bool, getspindle, setspindle, resetspindle)
|
||||
jograte_rate = Property(bool, getjograte, setjograte, resetjograte)
|
||||
jograte_angular_rate = Property(bool, getjograte_angular, setjograte_angular, resetjograte_angular)
|
||||
max_velocity_rate = Property(bool, getmax_velocity, setmax_velocity, resetmax_velocity)
|
||||
|
||||
def setAlertState(self, data):
|
||||
self._alertState = data
|
||||
def getAlertState(self):
|
||||
return self._alertState
|
||||
|
||||
def setAlertUnder(self, data):
|
||||
self._alertUnder = data
|
||||
def getAlertUnder(self):
|
||||
return self._alertUnder
|
||||
def resetAlertUnder(self):
|
||||
self._alertUnder = 50.0
|
||||
|
||||
def setAlertOver(self, data):
|
||||
self._alertOver = data
|
||||
def getAlertOver(self):
|
||||
return self._alertOver
|
||||
def resetAlertOver(self):
|
||||
self._alertOver = 100.0
|
||||
|
||||
alertState = Property(str, getAlertState, setAlertState)
|
||||
alertUnder = Property(float, getAlertUnder, setAlertUnder, resetAlertUnder)
|
||||
alertOver = Property(float, getAlertOver, setAlertOver, resetAlertOver)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
50
lib/python/qtvcp/widgets/status_stacked.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
#################################################################################
|
||||
|
||||
from qtpy import QtWidgets
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class StatusStacked(QtWidgets.QStackedWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(StatusStacked, self).__init__(parent)
|
||||
self.auto = True
|
||||
self.mdi = True
|
||||
self.manual = True
|
||||
|
||||
def _hal_init(self):
|
||||
def _switch(index):
|
||||
LOG.debug('index: {}'.format(index))
|
||||
self.setCurrentIndex(index)
|
||||
if self.auto:
|
||||
STATUS.connect('mode-auto', lambda w: _switch(2))
|
||||
if self.mdi:
|
||||
STATUS.connect('mode-mdi', lambda w: _switch(1))
|
||||
if self.manual:
|
||||
STATUS.connect('mode-manual', lambda w: _switch(0))
|
||||
324
lib/python/qtvcp/widgets/stylesheeteditor.py
Normal file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
############################################################################
|
||||
##
|
||||
## Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>.
|
||||
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
|
||||
## All rights reserved.
|
||||
##
|
||||
## This file is part of the examples of PyQt.
|
||||
##
|
||||
## $QT_BEGIN_LICENSE:BSD$
|
||||
## You may use this file under the terms of the BSD license as follows:
|
||||
##
|
||||
## "Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted provided that the following conditions are
|
||||
## met:
|
||||
## * Redistributions of source code must retain the above copyright
|
||||
## notice, this list of conditions and the following disclaimer.
|
||||
## * Redistributions in binary form must reproduce the above copyright
|
||||
## notice, this list of conditions and the following disclaimer in
|
||||
## the documentation and/or other materials provided with the
|
||||
## distribution.
|
||||
## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
|
||||
## the names of its contributors may be used to endorse or promote
|
||||
## products derived from this software without specific prior written
|
||||
## permission.
|
||||
##
|
||||
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
## $QT_END_LICENSE$
|
||||
##
|
||||
###########################################################################
|
||||
|
||||
import os
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Slot, QFile, QTextStream, QUrl, Qt
|
||||
from qtpy.QtGui import QStandardItem, QTextCursor, QTextCharFormat, QTextDocument, QColor
|
||||
from qtpy.QtWidgets import QDialog, QFileDialog, QMessageBox, QColorDialog
|
||||
|
||||
from qtvcp.core import Path
|
||||
from qtvcp.qt_makegui import VCPWindow
|
||||
from qtvcp import logger
|
||||
LOG = logger.getLogger(__name__)
|
||||
PATH = Path()
|
||||
WIDGETS = VCPWindow()
|
||||
|
||||
DATADIR = os.path.abspath( os.path.dirname( __file__ ) )
|
||||
|
||||
class StyleSheetEditor(QDialog):
|
||||
def __init__(self, parent=WIDGETS, path=None, addBuiltinStyles = True):
|
||||
super(StyleSheetEditor, self).__init__(parent)
|
||||
self.setMinimumSize(600, 400)
|
||||
# Load the widgets UI file:
|
||||
self.filename = os.path.join(PATH.SHAREDIR,'widgets_ui', 'style_dialog.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
self.styleSheetCombo.setFixedWidth(200)
|
||||
|
||||
self.setWindowTitle('Style Sheet Editor Dialog')
|
||||
self.addBuiltinStyles = addBuiltinStyles
|
||||
self.parent = parent
|
||||
if PATH:
|
||||
self.setPath()
|
||||
self.styleSheetCombo.currentIndexChanged.connect(self.selectionChanged)
|
||||
self.search_box.textChanged.connect(self.highlight_all_matches)
|
||||
self.btn_next.clicked.connect(self.find_next)
|
||||
self.btn_prev.clicked.connect(self.find_previous)
|
||||
self.preferencePath = 'DEFAULT'
|
||||
|
||||
def load_dialog(self):
|
||||
if WIDGETS.PREFS_:
|
||||
path = WIDGETS.PREFS_.getpref('style_QSS_Path', 'DEFAULT' , str, 'BOOK_KEEPING')
|
||||
self.preferencePath = path
|
||||
self.loadedItem.setData( path, role = Qt.UserRole + 1)
|
||||
self.lineEdit_path.setText(path)
|
||||
self.styleSheetCombo.setToolTip('<b>{}</b>'.format(path))
|
||||
self.origStyleSheet = self.parent.styleSheet()
|
||||
self.styleTextView.setPlainText(self.origStyleSheet)
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
|
||||
# keep a reference to loadedItem because the path will be added when the
|
||||
# dialog is shown.
|
||||
# Model holds a title and a path
|
||||
# search in the builtin folder for the screen and
|
||||
# in the users's config directory
|
||||
def setPath(self):
|
||||
model = self.styleSheetCombo.model()
|
||||
|
||||
# add an 'As Loaded' entry to follow the preference file's entry
|
||||
self.loadedItem = QStandardItem('As Loaded')
|
||||
self.loadedItem.setData( 'As Loaded', role = Qt.UserRole + 1)
|
||||
self.loadedItem.setData("Use the preference loaded Stylesheet", role = Qt.ToolTipRole)
|
||||
model.appendRow(self.loadedItem)
|
||||
|
||||
# add 'None' to cancel all stylesheet changes
|
||||
item = QStandardItem('None')
|
||||
item.setData( 'None', role = Qt.UserRole + 1)
|
||||
item.setData("Use system default Stylesheet", role = Qt.ToolTipRole)
|
||||
model.appendRow(item)
|
||||
|
||||
# call PATH function to get the found default and local qss files
|
||||
try:
|
||||
for group in (PATH.getQSSPaths(self.addBuiltinStyles)):
|
||||
for directory, name in(group):
|
||||
item = QStandardItem(name)
|
||||
item.setData(os.path.join(directory, name), role = Qt.UserRole + 1)
|
||||
item.setData(os.path.join(directory, name), role = Qt.ToolTipRole)
|
||||
model.appendRow(item)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def selectionChanged(self,i):
|
||||
self.search_box.clear()
|
||||
path = self.styleSheetCombo.itemData(i,role = Qt.UserRole + 1)
|
||||
name = self.styleSheetCombo.itemData(i,role = Qt.DisplayRole)
|
||||
self.styleSheetCombo.setToolTip('<b>{}</b>'.format(path))
|
||||
self.loadStyleSheet(path)
|
||||
|
||||
def highlight_all_matches(self):
|
||||
self.clear_highlights()
|
||||
|
||||
query = self.search_box.text()
|
||||
if not query:
|
||||
return
|
||||
|
||||
cursor = self.styleTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.Start)
|
||||
|
||||
format = QTextCharFormat()
|
||||
format.setBackground(QColor('#808080'))
|
||||
|
||||
self.matches = []
|
||||
while True:
|
||||
cursor = self.styleTextEdit.document().find(query, cursor)
|
||||
if cursor.isNull():
|
||||
break
|
||||
cursor.mergeCharFormat(format)
|
||||
self.matches.append(QTextCursor(cursor))
|
||||
|
||||
self.current_match_index = -1
|
||||
if self.matches:
|
||||
self.find_next()
|
||||
|
||||
def clear_highlights(self):
|
||||
cursor = self.styleTextEdit.textCursor()
|
||||
cursor.select(QTextCursor.Document)
|
||||
cursor.setCharFormat(QTextCharFormat())
|
||||
|
||||
def find_next(self):
|
||||
if not self.matches:
|
||||
return
|
||||
self.current_match_index = (self.current_match_index + 1) % len(self.matches)
|
||||
self.select_match()
|
||||
|
||||
def find_previous(self):
|
||||
if not self.matches:
|
||||
return
|
||||
self.current_match_index = (self.current_match_index - 1) % len(self.matches)
|
||||
self.select_match()
|
||||
|
||||
def select_match(self):
|
||||
cursor = self.matches[self.current_match_index]
|
||||
self.styleTextEdit.setTextCursor(cursor)
|
||||
|
||||
@Slot()
|
||||
def on_styleTextView_textChanged(self):
|
||||
self.applyButton.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
def on_applyButton_clicked(self):
|
||||
self.parent.setStyleSheet("")
|
||||
if self.tabWidget.currentIndex() == 0:
|
||||
self.styleTextEdit.clear()
|
||||
self.parent.setStyleSheet(self.styleTextView.toPlainText())
|
||||
if WIDGETS.PREFS_:
|
||||
index = self.styleSheetCombo.currentIndex()
|
||||
path = self.styleSheetCombo.itemData(index,role = Qt.UserRole + 1)
|
||||
WIDGETS.PREFS_.putpref('style_QSS_Path', path , str, 'BOOK_KEEPING')
|
||||
else:
|
||||
self.parent.setStyleSheet(self.styleTextEdit.toPlainText())
|
||||
|
||||
# styles can have affect on the dialog widgets
|
||||
# make sure one can still read the combo box
|
||||
self.styleSheetCombo.setFixedWidth(200)
|
||||
try:
|
||||
path = self.styleSheetCombo.itemData(index,role = Qt.UserRole + 1)
|
||||
self.parent.statusbar.showMessage(f"Stylesheet set to {path}")
|
||||
except:
|
||||
pass
|
||||
|
||||
@Slot()
|
||||
def on_openButton_clicked(self):
|
||||
if PATH.IS_SCREEN:
|
||||
DIR = PATH.SCREENDIR
|
||||
else:
|
||||
DIR = PATH.PANELDIR
|
||||
if os.path.exists(self.preferencePath):
|
||||
DIR = os.path.dirname(self.preferencePath)
|
||||
|
||||
dialog = QFileDialog(self)
|
||||
options = QFileDialog.Options()
|
||||
options |= QFileDialog.DontUseNativeDialog
|
||||
dialog.setOptions(options)
|
||||
dialog.setDirectory(DIR)
|
||||
dialog.setFileMode(QFileDialog.ExistingFile)
|
||||
dialog.setNameFilter("style files (*.qss *.style);;All files (*.*)")
|
||||
dialog.setWindowTitle('Open StyleSheet')
|
||||
|
||||
# sidebar links
|
||||
urls = []
|
||||
urls.append(QUrl.fromLocalFile(os.path.expanduser('~')))
|
||||
urls.append(QUrl.fromLocalFile(DIR))
|
||||
local = os.path.join(os.getcwd(),'qtvcp/screens',PATH.BASENAME)
|
||||
if os.path.exists(local):
|
||||
urls.append(QUrl.fromLocalFile(os.path.join(os.getcwd(),
|
||||
'qtvcp/screens',PATH.BASENAME)))
|
||||
dialog.setSidebarUrls(urls)
|
||||
|
||||
result = dialog.exec()
|
||||
if result:
|
||||
fileName = dialog.selectedFiles()[0]
|
||||
file = QFile(fileName)
|
||||
file.open(QFile.ReadOnly)
|
||||
styleSheet = file.readAll()
|
||||
self.styleTextView.setPlainText(str(styleSheet, encoding='utf8'))
|
||||
model = self.styleSheetCombo.model()
|
||||
item = QStandardItem(os.path.basename(fileName))
|
||||
item.setData( fileName, role = Qt.UserRole + 1)
|
||||
model.appendRow(item)
|
||||
self.styleSheetCombo.setCurrentIndex(self.styleSheetCombo.count()-1)
|
||||
|
||||
@Slot()
|
||||
def on_saveButton_clicked(self):
|
||||
if PATH.IS_SCREEN:
|
||||
DIR = PATH.SCREENDIR
|
||||
else:
|
||||
DIR = PATH.PANELDIR
|
||||
if os.path.exists(self.preferencePath):
|
||||
DIR = os.path.dirname(self.preferencePath)
|
||||
|
||||
dialog = QFileDialog(self)
|
||||
options = QFileDialog.Options()
|
||||
options |= QFileDialog.DontUseNativeDialog
|
||||
dialog.setOptions(options)
|
||||
dialog.setDirectory(DIR)
|
||||
dialog.setFileMode(QFileDialog.AnyFile)
|
||||
dialog.setNameFilter("style files (*.qss *.style);;All files (*.*)")
|
||||
dialog.setWindowTitle('Save StyleSheet')
|
||||
dialog.setLabelText( QFileDialog.Accept, "Save" )
|
||||
|
||||
# sidebar links
|
||||
urls = []
|
||||
urls.append(QUrl.fromLocalFile(os.path.expanduser('~')))
|
||||
urls.append(QUrl.fromLocalFile(DIR))
|
||||
urls.append(QUrl.fromLocalFile(os.path.join(os.getcwd(),'qtvcp/screens',PATH.BASENAME)))
|
||||
|
||||
dialog.setSidebarUrls(urls)
|
||||
|
||||
result = dialog.exec()
|
||||
if result:
|
||||
fileName = dialog.selectedFiles()[0]
|
||||
self.saveStyleSheet(fileName)
|
||||
|
||||
@Slot()
|
||||
def on_closeButton_clicked(self):
|
||||
self.close()
|
||||
|
||||
@Slot()
|
||||
def on_clearButton_clicked(self):
|
||||
self.styleTextEdit.clear()
|
||||
|
||||
@Slot()
|
||||
def on_copyButton_clicked(self):
|
||||
self.styleTextEdit.setPlainText(self.styleTextView.toPlainText())
|
||||
|
||||
@Slot()
|
||||
def on_colorButton_clicked(self):
|
||||
_color = QColorDialog.getColor()
|
||||
if _color.isValid():
|
||||
Color = _color.name()
|
||||
self.colorButton.setStyleSheet('QPushButton {background-color: %s ;}'% Color)
|
||||
self.styleTextEdit.insertPlainText(Color)
|
||||
|
||||
def loadStyleSheet(self, sheetName):
|
||||
if sheetName == 'None':
|
||||
self.styleTextView.setPlainText('')
|
||||
styleSheet = ''
|
||||
self.styleTextView.setPlainText(styleSheet)
|
||||
return
|
||||
if sheetName == 'As Loaded':
|
||||
styleSheet = self.origStyleSheet
|
||||
else:
|
||||
if PATH.IS_SCREEN:
|
||||
DIR = PATH.SCREENDIR
|
||||
BNAME = PATH.BASENAME
|
||||
else:
|
||||
DIR =PATH.PANELDIR
|
||||
BNAME = PATH.BASENAME
|
||||
qssname = os.path.join(DIR, BNAME, sheetName)
|
||||
self.lineEdit_path.setText(qssname)
|
||||
file = QFile(qssname)
|
||||
file.open(QFile.ReadOnly)
|
||||
styleSheet = file.readAll()
|
||||
self.styleTextView.setPlainText(str(styleSheet, encoding='utf8'))
|
||||
|
||||
def saveStyleSheet(self, fileName):
|
||||
styleSheet = self.styleTextEdit.toPlainText()
|
||||
file = QFile(fileName)
|
||||
if file.open(QFile.WriteOnly):
|
||||
QTextStream(file) << styleSheet
|
||||
else:
|
||||
QMessageBox.information(self, "Unable to open file",
|
||||
file.errorString())
|
||||
131
lib/python/qtvcp/widgets/system_tool_button.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# QTVcp Widget
|
||||
# Copyright (c) 2018 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.
|
||||
###############################################################################
|
||||
|
||||
|
||||
from qtpy.QtWidgets import QToolButton, QMenu, QAction
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.widgets.dialog_widget import EntryDialog
|
||||
from qtvcp.core import Status, Action, Info
|
||||
from qtvcp import logger
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# AUX_PRGM holds helper program loader
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class SystemToolButton(QToolButton, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(SystemToolButton, self).__init__(parent)
|
||||
self._joint = 0
|
||||
self._last = 0
|
||||
self._block_signal = False
|
||||
self._auto_label_flag = True
|
||||
SettingMenu = QMenu(self)
|
||||
for system in('G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3'):
|
||||
|
||||
Button = QAction(QIcon('exit24.png'), system, self)
|
||||
Button.triggered.connect(self[system.replace('.','_')])
|
||||
SettingMenu.addAction(Button)
|
||||
|
||||
self.setMenu(SettingMenu)
|
||||
self.dialog = EntryDialog()
|
||||
#self.setPopupMode(QToolButton.DelayedPopup)
|
||||
|
||||
def _hal_init(self):
|
||||
if not self.text() == '':
|
||||
self._auto_label_flag = False
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
|
||||
STATUS.connect('state-off', lambda w: self.menu().setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.menu().setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.menu().setEnabled(homed_on_test()))
|
||||
STATUS.connect('interp-run', lambda w: self.menu().setEnabled(False))
|
||||
STATUS.connect('all-homed', lambda w: self.menu().setEnabled(True))
|
||||
STATUS.connect('not-all-homed', lambda w, data: self.menu().setEnabled(False))
|
||||
STATUS.connect('interp-paused', lambda w: self.menu().setEnabled(False))
|
||||
STATUS.connect('user-system-changed', self._set_user_system_text)
|
||||
|
||||
def G54(self):
|
||||
ACTION.SET_USER_SYSTEM('G54')
|
||||
|
||||
def G55(self):
|
||||
ACTION.SET_USER_SYSTEM('G55')
|
||||
|
||||
def G56(self):
|
||||
ACTION.SET_USER_SYSTEM('G56')
|
||||
|
||||
def G57(self):
|
||||
ACTION.SET_USER_SYSTEM('G57')
|
||||
|
||||
def G58(self):
|
||||
ACTION.SET_USER_SYSTEM('G58')
|
||||
|
||||
def G59(self):
|
||||
ACTION.SET_USER_SYSTEM('G59')
|
||||
|
||||
def G59_1(self):
|
||||
ACTION.SET_USER_SYSTEM('G59.1')
|
||||
|
||||
def G59_2(self):
|
||||
ACTION.SET_USER_SYSTEM('G59.2')
|
||||
|
||||
def G59_3(self):
|
||||
ACTION.SET_USER_SYSTEM('G59.3')
|
||||
|
||||
def _set_user_system_text(self, w, data):
|
||||
convert = { 0:'None',1:"G54", 2:"G55", 3:"G56", 4:"G57", 5:"G58", 6:"G59", 7:"G59.1", 8:"G59.2", 9:"G59.3"}
|
||||
if self._auto_label_flag:
|
||||
self.setText(convert[int(data)])
|
||||
|
||||
def ChangeState(self, joint):
|
||||
if int(joint) != self._joint:
|
||||
self._block_signal = True
|
||||
self.setChecked(False)
|
||||
self._block_signal = False
|
||||
self.hal_pin.set(False)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
widget = SystemToolButton()
|
||||
widget.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
lib/python/qtvcp/widgets/tab_widget.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtVcp tab widgets with adjustable tab height
|
||||
#
|
||||
# Copyright (c) 2019 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
|
||||
from qtpy.QtWidgets import (QTabWidget, QTabBar)
|
||||
from qtpy.QtCore import Property, QSize
|
||||
|
||||
class TabWidget(QTabWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(TabWidget, self).__init__(parent)
|
||||
self._tabSize = 1.5
|
||||
self.setTabBar(TabBar(size=self._tabSize))
|
||||
#self.setCornerWidget(QPushButton("hi"))
|
||||
|
||||
def set_tabSize(self, data):
|
||||
self._tabSize = data
|
||||
def get_tabSize(self):
|
||||
return self._tabSize
|
||||
def reset_tabSize(self):
|
||||
self._tabSize = 1.5
|
||||
|
||||
tabSize = Property(float, get_tabSize, set_tabSize, reset_tabSize)
|
||||
|
||||
class TabBar(QTabBar):
|
||||
def __init__(self, parent=None, size=1.5):
|
||||
super(TabBar, self).__init__(parent)
|
||||
self._size=size
|
||||
|
||||
def tabSizeHint(self, index):
|
||||
size = QTabBar.tabSizeHint(self, index)
|
||||
w = size.height()*self._size
|
||||
return QSize(size.width(), int(w))
|
||||
|
||||
182
lib/python/qtvcp/widgets/tool_chooser.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp
|
||||
#
|
||||
# Copyright (c) 2025 Steve Richardson
|
||||
#
|
||||
# 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 sys
|
||||
import os
|
||||
import operator
|
||||
from qtpy import QtGui, QtWidgets, uic
|
||||
from qtpy.QtCore import Qt, QAbstractTableModel
|
||||
from qtpy.QtGui import QColor, QIcon
|
||||
from qtpy.QtWidgets import (QTableView, QAbstractItemView, QCheckBox, QStyledItemDelegate, QHeaderView)
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info, Tool
|
||||
from qtvcp import logger
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
TOOL = Tool()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
class ToolChooser(QTableView, _HalWidgetBase):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ToolChooser, self).__init__(parent)
|
||||
self._current_tool = -1
|
||||
self.createAllView()
|
||||
|
||||
def _hal_init(self):
|
||||
self.delay = 0
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
STATUS.connect('not-all-homed', lambda w, axis: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w, data: self.currentTool(data))
|
||||
|
||||
for i in (0,2,3,4,5,6,7,8,9,10,11,12,13,14,16,17,18):
|
||||
self.hideColumn(i)
|
||||
|
||||
def currentTool(self, tool):
|
||||
self._current_tool = tool
|
||||
|
||||
def createAllView(self):
|
||||
styledItemDelegate=QStyledItemDelegate()
|
||||
self.setItemDelegate(styledItemDelegate)
|
||||
|
||||
self.tablemodel = ToolTableModel(self)
|
||||
self.setModel(self.tablemodel)
|
||||
self.setMinimumSize(100, 100)
|
||||
|
||||
hh = self.horizontalHeader()
|
||||
hh.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
hh.setStretchLastSection(True)
|
||||
hh.setSortIndicator(1,Qt.AscendingOrder)
|
||||
|
||||
vh = self.verticalHeader()
|
||||
vh.setVisible(False)
|
||||
self.resizeColumnsToContents()
|
||||
self.resizeRowsToContents()
|
||||
self.setSortingEnabled(True)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
|
||||
def getSelectedTool(self):
|
||||
index = self.selectionModel().currentIndex()
|
||||
return self.tablemodel.arraydata[index.row()][1]
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
|
||||
#########################################
|
||||
# model holds the tool data
|
||||
#########################################
|
||||
class ToolTableModel(QAbstractTableModel):
|
||||
def __init__(self, parent=None):
|
||||
super(ToolTableModel, self).__init__(parent)
|
||||
self.text_template = '%.4f'
|
||||
self._current_tool_bg_color = '#00ffff'
|
||||
self._current_tool_color = '#777777'
|
||||
self._selectedcolor = '#00ff00'
|
||||
self.headerdata = ['','tool','pocket','X','X Wear', 'Y', 'Y Wear', 'Z', 'Z Wear', 'A', 'B', 'C', 'U', 'V', 'W', 'Diameter', 'Front Angle', 'Back Angle','Orient','Comment']
|
||||
if INFO.MACHINE_IS_LATHE:
|
||||
self.headerdata[2] = 'Stn'
|
||||
self.vheaderdata = []
|
||||
self.arraydata = [[0,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0,'No Tool']]
|
||||
STATUS.connect('toolfile-stale',lambda o, d: self.update(d))
|
||||
self.update(None)
|
||||
|
||||
def update(self, models):
|
||||
data = TOOL.CONVERT_TO_WEAR_TYPE(models)
|
||||
if data in (None, []):
|
||||
data = [[0,0,0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0,'No Tool']]
|
||||
self.arraydata = data
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.arraydata)
|
||||
|
||||
def columnCount(self, parent):
|
||||
if len(self.arraydata) > 0:
|
||||
return len(self.arraydata[0])
|
||||
return 0
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
if isinstance(value, str):
|
||||
return '%s' % value
|
||||
return value
|
||||
|
||||
elif role == Qt.BackgroundRole:
|
||||
if self.arraydata[index.row()][1] == self.parent()._current_tool:
|
||||
return QColor(self._current_tool_bg_color)
|
||||
return None
|
||||
|
||||
elif role == Qt.ForegroundRole:
|
||||
if self.arraydata[index.row()][1] == self.parent()._current_tool:
|
||||
return QColor(self._current_tool_color)
|
||||
return None
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return None
|
||||
else:
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
def headerData(self, col, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.headerdata[col]
|
||||
elif orientation != Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return ''
|
||||
return None
|
||||
|
||||
def sort(self, Ncol, order):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
|
||||
if order == Qt.DescendingOrder:
|
||||
self.arraydata.reverse()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
#############################
|
||||
# Testing #
|
||||
#############################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = ToolChooser()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
663
lib/python/qtvcp/widgets/tool_offsetview.py
Normal file
@@ -0,0 +1,663 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Qtvcp widget
|
||||
# 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 sys
|
||||
import os
|
||||
import operator
|
||||
|
||||
from qtpy.QtCore import Qt, QAbstractTableModel, Property, QSize, Slot
|
||||
from qtpy.QtGui import QColor, QIcon
|
||||
from qtpy.QtWidgets import (QTableView, QAbstractItemView, QCheckBox,
|
||||
QItemEditorFactory,QDoubleSpinBox,QSpinBox,QStyledItemDelegate, QApplication, QHeaderView)
|
||||
|
||||
# QVariant type IDs for ItemEditorFactory: use QVariant constants on PyQt5,
|
||||
# fall back to the stable Qt metatype integers on PyQt6 where they were removed.
|
||||
try:
|
||||
from qtpy.QtCore import QVariant as _QVariant
|
||||
_DOUBLE_TYPE = _QVariant.Double
|
||||
_INT_TYPE = _QVariant.Int
|
||||
except AttributeError:
|
||||
_DOUBLE_TYPE = 6 # QMetaType::Double
|
||||
_INT_TYPE = 2 # QMetaType::Int
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info, Tool, Path
|
||||
from qtvcp import logger
|
||||
|
||||
#BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), ".."))
|
||||
#LOCALEDIR = os.path.join(BASE, "share", "locale")
|
||||
#locale.setlocale(locale.LC_ALL, '')
|
||||
|
||||
# Instiniate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# INI holds ini details
|
||||
# ACTION gives commands to linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
TOOL = Tool()
|
||||
PATH = Path()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
ICONPATH = os.path.join(PATH.SHAREDIR, 'images/widgets/tool_offsetview')
|
||||
|
||||
# custom spinbox controls for editing
|
||||
class ItemEditorFactory(QItemEditorFactory):
|
||||
def __init__(self):
|
||||
super(ItemEditorFactory,self).__init__()
|
||||
|
||||
def createEditor(self, userType, parent):
|
||||
if userType == _DOUBLE_TYPE:
|
||||
doubleSpinBox = QDoubleSpinBox(parent)
|
||||
doubleSpinBox.setDecimals(4)
|
||||
doubleSpinBox.setMaximum(99999)
|
||||
doubleSpinBox.setMinimum(-99999)
|
||||
return doubleSpinBox
|
||||
elif userType == _INT_TYPE:
|
||||
spinBox = QSpinBox(parent)
|
||||
spinBox.setMaximum(20000)
|
||||
spinBox.setMinimum(1)
|
||||
return spinBox
|
||||
else:
|
||||
return super(ItemEditorFactory,self).createEditor(userType, parent)
|
||||
|
||||
class ToolOffsetView(QTableView, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(ToolOffsetView, self).__init__(parent)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.filename = INFO.PARAMETER_FILE
|
||||
self.axisletters = ["x", "y", "z", "a", "b", "c", "u", "v", "w"]
|
||||
self.editing_flag = False
|
||||
self.current_system = None
|
||||
self.current_tool = 0
|
||||
self.setEnabled(False)
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.text_dialog_code = 'KEYBOARD'
|
||||
self.setIconSize(QSize(32,32))
|
||||
self._last = 0
|
||||
|
||||
# create table
|
||||
self.createAllView()
|
||||
|
||||
def _hal_init(self):
|
||||
self.delay = 0
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(True))
|
||||
STATUS.connect('not-all-homed', lambda w, axis: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(STATUS.machine_is_on()
|
||||
and (STATUS.is_all_homed()
|
||||
or INFO.NO_HOME_REQUIRED)))
|
||||
STATUS.connect('interp-run', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('metric-mode-changed', lambda w, data: self.metricMode(data))
|
||||
STATUS.connect('diameter-mode', lambda w, data: self.diameterMode(data))
|
||||
STATUS.connect('tool-in-spindle-changed', lambda w, data: self.currentTool(data))
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
conversion = {5:"Y", 6:'Y', 7:"Z", 8:'Z', 9:"A", 10:"B", 11:"C", 12:"U", 13:"V", 14:"W"}
|
||||
for num, let in conversion.items():
|
||||
if let in (INFO.AVAILABLE_AXES):
|
||||
continue
|
||||
self.hideColumn(num)
|
||||
if not INFO.MACHINE_IS_LATHE:
|
||||
for i in (4,6,8,16,17,18):
|
||||
self.hideColumn(i)
|
||||
|
||||
def currentTool(self, data):
|
||||
self.current_tool = data
|
||||
|
||||
def metricMode(self, state):
|
||||
self.tablemodel.metricDisplay(state)
|
||||
self.update()
|
||||
|
||||
def diameterMode(self, state):
|
||||
self.tablemodel.diameterDisplay(state)
|
||||
self.update()
|
||||
|
||||
def createAllView(self):
|
||||
styledItemDelegate=QStyledItemDelegate()
|
||||
styledItemDelegate.setItemEditorFactory(ItemEditorFactory())
|
||||
self.setItemDelegate(styledItemDelegate)
|
||||
|
||||
# create the view
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
#self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
||||
# set the table model
|
||||
self.tablemodel = MyTableModel(self)
|
||||
self.setModel(self.tablemodel)
|
||||
self.clicked.connect(self.showSelection)
|
||||
#self.dataChanged.connect(self.selection_changed)
|
||||
|
||||
# set the minimum size
|
||||
self.setMinimumSize(100, 100)
|
||||
|
||||
# set horizontal header properties
|
||||
hh = self.horizontalHeader()
|
||||
# auto adjust to contents
|
||||
hh.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
|
||||
hh.setStretchLastSection(True)
|
||||
hh.setSortIndicator(1,Qt.AscendingOrder)
|
||||
|
||||
vh = self.verticalHeader()
|
||||
vh.setVisible(False)
|
||||
|
||||
# set column width to fit contents
|
||||
self.resizeColumnsToContents()
|
||||
|
||||
# set row height
|
||||
self.resizeRowsToContents()
|
||||
|
||||
# enable sorting
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def showSelection(self, item):
|
||||
cellContent = item.data()
|
||||
text = cellContent
|
||||
LOG.debug('Text: {}, Row: {}, Column: {}'.format(text, item.row(), item.column()))
|
||||
sf = "You clicked on {}".format(text)
|
||||
# display in title bar for convenience
|
||||
self.setWindowTitle(sf)
|
||||
# row 0 is not editable (checkbox position)
|
||||
# column 19 is the descriptive text column
|
||||
if item.column() == 19:
|
||||
self.callTextDialog(text,item)
|
||||
elif item.column() <19 and item.column() > 0:
|
||||
self.callDialog(text,item)
|
||||
|
||||
# alphanumerical
|
||||
def callTextDialog(self, text,item):
|
||||
text = self.tablemodel.arraydata[item.row()][19]
|
||||
tool = self.tablemodel.arraydata[item.row()][1]
|
||||
mess = {'NAME':self.text_dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'PRELOAD':text, 'TITLE':'Tool {} Description Entry'.format(tool),
|
||||
'ITEM':item}
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
# numerical only
|
||||
def callDialog(self, text, item, next=False):
|
||||
axis = self.tablemodel.headerdata[item.column()]
|
||||
tool = self.tablemodel.arraydata[item.row()][1]
|
||||
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'PRELOAD':float(text),
|
||||
'TITLE':'Tool {} Offset of {},{}'.format(tool, axis,text),
|
||||
'ITEM':item,
|
||||
'NEXT':next,
|
||||
'WIDGETCYCLE': True}
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
STATUS.emit('dialog-request', mess)
|
||||
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
name2 = bool(message.get('NAME') == self.text_dialog_code)
|
||||
item = message.get('ITEM')
|
||||
next = message.get('NEXT', False)
|
||||
back = message.get('BACK', False)
|
||||
|
||||
if code:
|
||||
LOG.debug('message returned:{}'.format (message))
|
||||
if code and name and num is not None:
|
||||
self.tablemodel.setData(item, num, None)
|
||||
elif code and name2 and num is not None:
|
||||
self.tablemodel.setData(item, num, None)
|
||||
|
||||
# request for next input widget to right or left
|
||||
if code and name:
|
||||
|
||||
if next:
|
||||
self.right()
|
||||
|
||||
newobj = self.currentIndex()
|
||||
# if we selected the text column, move back
|
||||
if newobj.column() == 19:
|
||||
self.left()
|
||||
newobj = self.currentIndex()
|
||||
cellContent = newobj.data()
|
||||
text = cellContent
|
||||
|
||||
# update the screen
|
||||
QApplication.processEvents()
|
||||
|
||||
# update the dialog
|
||||
self.callDialog(text,newobj,True)
|
||||
|
||||
elif back:
|
||||
self.left()
|
||||
|
||||
newobj = self.currentIndex()
|
||||
# if we selected the checkbox column, move forward
|
||||
if newobj.column() == 0:
|
||||
self.right()
|
||||
newobj = self.currentIndex()
|
||||
|
||||
newobj = self.currentIndex()
|
||||
cellContent = newobj.data()
|
||||
text = cellContent
|
||||
|
||||
# update the screen
|
||||
QApplication.processEvents()
|
||||
|
||||
# update the dialog
|
||||
self.callDialog(text,newobj,True)
|
||||
|
||||
#############################################################
|
||||
|
||||
def dataChanged(self, new, old, roles):
|
||||
self.editing_flag = True
|
||||
row = new.row()
|
||||
col = new.column()
|
||||
data = self.tablemodel.data(new)
|
||||
#print('Entered data:', data, row,col)
|
||||
# now update linuxcnc to the change
|
||||
try:
|
||||
if STATUS.is_status_valid():
|
||||
#for i in self.tablemodel.arraydata:
|
||||
# LOG.debug("2>>> = {}".format(i))
|
||||
error = TOOL.SAVE_TOOLFILE(TOOL.CONVERT_TO_STANDARD_TYPE(self.tablemodel.arraydata))
|
||||
if error:
|
||||
raise
|
||||
ACTION.RECORD_CURRENT_MODE()
|
||||
ACTION.CALL_MDI('g43')
|
||||
ACTION.RESTORE_RECORDED_MODE()
|
||||
STATUS.emit('reload-display')
|
||||
#self.tablemodel.update()
|
||||
#self.resizeColumnsToContents()
|
||||
except Exception as e:
|
||||
LOG.exception("offsetpage widget error: MDI call error", exc_info=e)
|
||||
self.editing_flag = False
|
||||
|
||||
def add_tool(self):
|
||||
if not STATUS.is_auto_running():
|
||||
LOG.debug('add tool request')
|
||||
TOOL.ADD_TOOL()
|
||||
|
||||
def delete_tools(self):
|
||||
if not STATUS.is_auto_running():
|
||||
LOG.debug('delete tools request')
|
||||
dtools = self.tablemodel.listCheckedTools()
|
||||
if dtools:
|
||||
error = TOOL.DELETE_TOOLS(dtools)
|
||||
|
||||
def get_checked_list(self):
|
||||
return self.tablemodel.listCheckedTools()
|
||||
|
||||
def set_all_unchecked(self):
|
||||
self.tablemodel.uncheckAllTools()
|
||||
|
||||
# This function uses the color name (string); setProperty
|
||||
# expects a QColor object
|
||||
def highlight(self, color):
|
||||
self.setProperty('styleColorHighlight', QColor(color))
|
||||
|
||||
# This function uses the color name (string); calling setProperty
|
||||
# expects a QColor object
|
||||
def selected(self, color):
|
||||
self.setProperty('styleColorSelection', QColor(color))
|
||||
|
||||
# external controls
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def scroll(self, data):
|
||||
if data > self._last:
|
||||
self.right()
|
||||
elif data < self._last:
|
||||
self.right()
|
||||
self._last = data
|
||||
|
||||
# moves the selection up
|
||||
@Slot()
|
||||
def up(self):
|
||||
cr = self.currentIndex().row()
|
||||
# state of checkbox ie. is this row selected?
|
||||
# if selected, move horizontally
|
||||
state = self.model().arraydata[cr][0].isChecked()
|
||||
if state:
|
||||
self.right()
|
||||
return
|
||||
else:
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveUp,Qt.NoModifier))
|
||||
|
||||
# moves the selection down
|
||||
@Slot()
|
||||
def down(self):
|
||||
cr = self.currentIndex().row()
|
||||
# state of checkbox ie. is this row selected?
|
||||
# if selected, move horizontally
|
||||
state = self.model().arraydata[cr][0].isChecked()
|
||||
if state:
|
||||
self.left()
|
||||
return
|
||||
else:
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveDown,Qt.NoModifier))
|
||||
|
||||
def left(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveLeft,Qt.NoModifier))
|
||||
|
||||
def right(self):
|
||||
self.setCurrentIndex(self.moveCursor(QAbstractItemView.CursorAction.MoveRight,Qt.NoModifier))
|
||||
|
||||
def toggleCurrent(self):
|
||||
self.model().toggle(self.currentIndex().row())
|
||||
|
||||
#########################################################################
|
||||
# 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 set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialog_code_string = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_keyboard_code(self, data):
|
||||
self.text_dialog_code = data
|
||||
def get_keyboard_code(self):
|
||||
return self.text_dialog_code
|
||||
def reset_keyboard_code(self):
|
||||
self.text_dialog_code = 'KEYBOARD'
|
||||
text_dialog_code_string = Property(str, get_keyboard_code, set_keyboard_code, reset_keyboard_code)
|
||||
|
||||
def setmetrictemplate(self, data):
|
||||
self.tablemodel.metric_text_template = data
|
||||
def getmetrictemplate(self):
|
||||
return self.tablemodel.metric_text_template
|
||||
def resetmetrictemplate(self):
|
||||
self.tablemodel.metric_text_template = '%10.3f'
|
||||
metric_template = Property(str, getmetrictemplate, setmetrictemplate, resetmetrictemplate)
|
||||
|
||||
def setimperialtexttemplate(self, data):
|
||||
self.tablemodel.imperial_text_template = data
|
||||
def getimperialtexttemplate(self):
|
||||
return self.tablemodel.imperial_text_template
|
||||
def resetimperialtexttemplate(self):
|
||||
self.tablemodel.imperial_text_template = '%9.4f'
|
||||
imperial_template = Property(str, getimperialtexttemplate, setimperialtexttemplate, resetimperialtexttemplate)
|
||||
|
||||
def getColorHighlight(self):
|
||||
return QColor(self.tablemodel._highlightcolor)
|
||||
def setColorHighlight(self, value):
|
||||
self.tablemodel._highlightcolor = value.name()
|
||||
#self.tablemodel.layoutChanged.emit()
|
||||
styleColorHighlight = Property(QColor, getColorHighlight, setColorHighlight)
|
||||
|
||||
def getColorSelection(self):
|
||||
return QColor(self.tablemodel._selectedcolor)
|
||||
def setColorSelection(self, value):
|
||||
self.tablemodel._selectedcolor = value.name()
|
||||
#self.tablemodel.layoutChanged.emit()
|
||||
styleColorSelection = Property(QColor, getColorSelection, setColorSelection)
|
||||
|
||||
#########################################
|
||||
# custom model
|
||||
#########################################
|
||||
class MyTableModel(QAbstractTableModel):
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Args:
|
||||
datain: a list of lists\n
|
||||
headerdata: a list of strings
|
||||
"""
|
||||
super(MyTableModel, self).__init__(parent)
|
||||
self.text_template = '%.4f'
|
||||
self.metric_text_template = '%10.3f'
|
||||
self.zero_text_template = '%10.1f'
|
||||
self.imperial_text_template = '%9.4f'
|
||||
self.degree_text_template = '%10.1f'
|
||||
self.metric_display = False
|
||||
self.diameter_display = False
|
||||
self._highlightcolor = '#00ffff'
|
||||
self._selectedcolor = '#00ff00'
|
||||
self.headerdata = ['','tool','pocket','X','X Wear', 'Y', 'Y Wear', 'Z', 'Z Wear', 'A', 'B', 'C', 'U', 'V', 'W', 'Diameter', 'Front Angle', 'Back Angle','Orient','Comment']
|
||||
if INFO.MACHINE_IS_LATHE:
|
||||
self.headerdata[2] = 'Stn'
|
||||
self.vheaderdata = []
|
||||
self.arraydata = [[0, 0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0,'No Tool']]
|
||||
STATUS.connect('toolfile-stale',lambda o, d: self.update(d))
|
||||
self.update(None)
|
||||
|
||||
def metricDisplay(self, state):
|
||||
self.metric_display = state
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def diameterDisplay(self, state):
|
||||
self.diameter_display = state
|
||||
self.layoutChanged.emit()
|
||||
|
||||
# make a list of all the checked tools
|
||||
def listCheckedTools(self):
|
||||
checkedlist = []
|
||||
for row in self.arraydata:
|
||||
if row[0].isChecked():
|
||||
checkedlist.append(row[1])
|
||||
return checkedlist
|
||||
|
||||
def uncheckAllTools(self):
|
||||
for row in self.arraydata:
|
||||
if row[0].isChecked():
|
||||
row[0].setChecked(False)
|
||||
|
||||
def toggle(self, row):
|
||||
item = self.arraydata[row][0]
|
||||
newState = (not item.isChecked())
|
||||
self.uncheckAllTools()
|
||||
item.setChecked(newState)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
# update the internal array from STATUS's toolfile read array
|
||||
# we make sure the first array is switched to a QCheckbox widget
|
||||
def update(self, models):
|
||||
data = TOOL.CONVERT_TO_WEAR_TYPE(models)
|
||||
if data in (None, []):
|
||||
data = [[QCheckBox(),0, 0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 0,'No Tool']]
|
||||
for line in data:
|
||||
if line[0] != QCheckBox:
|
||||
line[0] = QCheckBox()
|
||||
self.arraydata = data
|
||||
self.layoutChanged.emit()
|
||||
|
||||
# Returns the number of rows under the given parent.
|
||||
# When the parent is valid it means that rowCount is
|
||||
# returning the number of children of parent.
|
||||
#
|
||||
# Note: When implementing a table based model, rowCount()
|
||||
# should return 0 when the parent is valid.
|
||||
def rowCount(self, parent):
|
||||
return len(self.arraydata)
|
||||
|
||||
# Returns the number of columns for the children of the given parent.
|
||||
# Note: When implementing a table based model, columnCount() should
|
||||
# return 0 when the parent is valid.
|
||||
def columnCount(self, parent):
|
||||
if len(self.arraydata) > 0:
|
||||
return len(self.arraydata[0])
|
||||
return 0
|
||||
|
||||
# Returns the data stored under the given role for the item referred to by the index.
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
|
||||
if role == Qt.EditRole:
|
||||
return self.arraydata[index.row()][index.column()]
|
||||
|
||||
elif role == Qt.DecorationRole and index.column() == 18:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
return QIcon(os.path.join(ICONPATH, "tool_pos_{}.png".format(value)))
|
||||
|
||||
elif role == Qt.DisplayRole:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
col = index.column()
|
||||
if isinstance(value, float):
|
||||
if value == 0.0:
|
||||
tmpl = lambda s: self.zero_text_template % s
|
||||
return tmpl(value)
|
||||
elif col in(16,17):
|
||||
tmpl = lambda s: self.degree_text_template % s
|
||||
return tmpl(value)
|
||||
elif self.metric_display:
|
||||
tmpl = lambda s: self.metric_text_template % s
|
||||
else:
|
||||
tmpl = lambda s: self.imperial_text_template % s
|
||||
if self.metric_display != INFO.MACHINE_IS_METRIC:
|
||||
value = INFO.convert_units(value)
|
||||
if col in(3,4):
|
||||
if self.diameter_display:
|
||||
value *=2
|
||||
self.headerdata[3] = 'X D'
|
||||
self.headerdata[4] = 'X Wear D'
|
||||
else:
|
||||
self.headerdata[3] = 'X R'
|
||||
self.headerdata[4] = 'X Wear R'
|
||||
return tmpl(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return '%s' % value
|
||||
# Default (anything not captured above: e.g. int)
|
||||
return value
|
||||
|
||||
elif role == Qt.BackgroundRole:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
if (isinstance(value, int) or isinstance(value, float) or
|
||||
isinstance(value, str), isinstance(value, QCheckBox)):
|
||||
if self.arraydata[index.row()][1] == self.parent().current_tool:
|
||||
return QColor(self._highlightcolor)
|
||||
elif self.arraydata[index.row()][0].isChecked():
|
||||
return QColor(self._selectedcolor)
|
||||
else:
|
||||
return None
|
||||
|
||||
elif role == Qt.CheckStateRole:
|
||||
if index.column() == 0:
|
||||
# print(">>> data() row,col = %d, %d" % (index.row(), index.column()))
|
||||
if self.arraydata[index.row()][index.column()].isChecked():
|
||||
return Qt.Checked
|
||||
else:
|
||||
return Qt.Unchecked
|
||||
|
||||
elif role == Qt.ForegroundRole:
|
||||
value = self.arraydata[index.row()][index.column()]
|
||||
|
||||
if ((isinstance(value, int) or isinstance(value, float))
|
||||
and value < 0 ):
|
||||
return QColor('red')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Returns the item flags for the given index.
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return None
|
||||
if index.column() == 0:
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable
|
||||
else:
|
||||
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
# Sets the role data for the item at index to value.
|
||||
# Returns true if successful; otherwise returns false.
|
||||
# The dataChanged() signal should be emitted if the data was successfully set.
|
||||
# these column numbers correspond to our included wear columns
|
||||
# it will be converted when saved
|
||||
def setData(self, index, value, role):
|
||||
col = index.column()
|
||||
if not index.isValid():
|
||||
LOG.error(">>> index not valid {}".format(index))
|
||||
return False
|
||||
LOG.debug("original value:{}".format(self.arraydata[index.row()][col]))
|
||||
LOG.debug(">>> setData() role = {}".format(role))
|
||||
LOG.debug(">>> setData() column() = {}".format(col))
|
||||
if role == Qt.CheckStateRole and index.column() == 0:
|
||||
#print(">>> setData() role = ", role)
|
||||
#print(">>> setData() index.column() = ", index.column())
|
||||
if value == Qt.Checked:
|
||||
self.uncheckAllTools()
|
||||
self.arraydata[index.row()][index.column()].setChecked(True)
|
||||
#self.arraydata[index.row()][index.column()].setText("Delete")
|
||||
#print 'selected',self.arraydata[index.row()][1]
|
||||
else:
|
||||
self.arraydata[index.row()][index.column()].setChecked(False)
|
||||
#self.arraydata[index.row()][index.column()].setText("Un")
|
||||
# don't emit dataChanged - return right away
|
||||
self.parent().reset()
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
if col in (1,2,18): # tool, pocket, orientation
|
||||
v = int(value)
|
||||
elif col == 19:
|
||||
v = str(value) # comment
|
||||
else:
|
||||
v = float(value)
|
||||
if self.metric_display:
|
||||
v = INFO.convert_metric_to_machine(value)
|
||||
else:
|
||||
v = INFO.convert_imperial_to_machine(value)
|
||||
if col in(3,4) and self.diameter_display:
|
||||
v /=2
|
||||
self.arraydata[index.row()][col] = v
|
||||
except:
|
||||
LOG.error("Invalid data type in row {} column:{} ".format(index.row(), col))
|
||||
return False
|
||||
LOG.debug(">>> setData() value = {} ".format(value))
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
|
||||
# Returns the data for the given role and section in the header with the specified orientation.
|
||||
# For horizontal headers, the section number corresponds to the column number.
|
||||
# Similarly, for vertical headers, the section number corresponds to the row number.
|
||||
def headerData(self, col, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return self.headerdata[col]
|
||||
if orientation != Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return ''
|
||||
return None
|
||||
|
||||
# Sorts the model by column in the given order.
|
||||
def sort(self, Ncol, order):
|
||||
"""
|
||||
Sort table by given column number.
|
||||
"""
|
||||
# don't sort checkbox column
|
||||
if Ncol != 0:
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
|
||||
if order == Qt.DescendingOrder:
|
||||
self.arraydata.reverse()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
w = ToolOffsetView()
|
||||
w.setEnabled(True)
|
||||
w._hal_init()
|
||||
w.highlight('lightblue')
|
||||
#w.setProperty('styleColorHighlight',QColor('purple'))
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
285
lib/python/qtvcp/widgets/touchEntry.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from qtpy.QtCore import Property, Signal, QSize, QObject, Qt
|
||||
from qtpy.QtGui import QDoubleValidator, QIntValidator
|
||||
from qtpy.QtWidgets import QWidget, QPushButton,QLineEdit, QHBoxLayout
|
||||
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class LineEdit(QLineEdit):
|
||||
|
||||
clicked = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
#self.clicked.connect(lambda :self.entryClicked())
|
||||
|
||||
def mousePressEvent(self,event):
|
||||
self.clicked.emit(True)
|
||||
|
||||
class TouchSpinBox(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._value = 0
|
||||
self._multiplierIndex = 0
|
||||
|
||||
self.setMinimumSize(QSize(200, 25))
|
||||
self.setMaximumSize(QSize(300, 50))
|
||||
|
||||
lay = QHBoxLayout()
|
||||
lay.setContentsMargins(0,0,0,0)
|
||||
lay.setSpacing(0)
|
||||
self.setLayout(lay)
|
||||
|
||||
self.entry = LineEdit()
|
||||
self.entry.setMaximumHeight(100)
|
||||
self.entry.setMinimumWidth(100)
|
||||
|
||||
self.entry.clicked.connect(lambda :self.callDialog(self))
|
||||
lay.addWidget(self.entry)
|
||||
|
||||
up = QPushButton('Up')
|
||||
up.setMaximumHeight(100)
|
||||
up.setMaximumWidth(40)
|
||||
up.pressed.connect(lambda :self.updateValue(True))
|
||||
lay.addWidget(up)
|
||||
|
||||
self.select = QPushButton(str('<-'))
|
||||
self.select.setMaximumHeight(100)
|
||||
self.select.setMaximumWidth(40)
|
||||
self.select.pressed.connect(lambda :self.moveLeft())
|
||||
lay.addWidget(self.select)
|
||||
|
||||
self.selectR = QPushButton(str('->'))
|
||||
self.selectR.setMaximumHeight(100)
|
||||
self.selectR.setMaximumWidth(40)
|
||||
self.selectR.pressed.connect(lambda :self.moveRight())
|
||||
lay.addWidget(self.selectR)
|
||||
|
||||
self.down = QPushButton('Dwn')
|
||||
self.down.setMaximumHeight(100)
|
||||
self.down.setMaximumWidth(40)
|
||||
self.down.pressed.connect(lambda :self.updateValue(False))
|
||||
lay.addWidget(self.down)
|
||||
|
||||
self.init_settings()
|
||||
|
||||
def init_settings(self):
|
||||
self.entry.setText(str('00'))
|
||||
self.validInt = QIntValidator(-99999, 99999)
|
||||
self.entry.setValidator(self.validInt)
|
||||
|
||||
def updateValue(self, state):
|
||||
mult = pow(10,self._multiplierIndex)
|
||||
print('update mult:',mult)
|
||||
decimal.getcontext().prec = 8
|
||||
x = Decimal(self._value) + Decimal( mult)
|
||||
print('Dec:',x)
|
||||
if state:
|
||||
self.setValue(Decimal(self._value) + Decimal( mult))
|
||||
else:
|
||||
self.setValue(Decimal(self._value) - Decimal( mult))
|
||||
|
||||
def moveLeft(self):
|
||||
mult = self._multiplierIndex + 1
|
||||
t = len(self.entry.text())
|
||||
print('mult',mult, 't',t)
|
||||
if t < 0:t=0
|
||||
if mult >= t:
|
||||
mult = 0
|
||||
|
||||
pwr = pow(10,mult)
|
||||
print('pow',pwr)
|
||||
|
||||
self._multiplierIndex = mult
|
||||
#self.select.setText(str(pow(10,mult)))
|
||||
print('left',t-self._multiplierIndex-1,mult)
|
||||
self.entry.setSelection (t-self._multiplierIndex-1,1)
|
||||
|
||||
def moveRight(self):
|
||||
mult = self._multiplierIndex - 1
|
||||
t = len(self.entry.text())
|
||||
print('mult',mult, 't',t)
|
||||
if t < 0:t=0
|
||||
if mult < 0:
|
||||
mult = t-1
|
||||
|
||||
pwr = pow(10,mult)
|
||||
print('pow',pwr)
|
||||
|
||||
self._multiplierIndex = mult
|
||||
#self.select.setText(str(pow(10,mult)))
|
||||
print('right',t-self._multiplierIndex-1,mult)
|
||||
self.entry.setSelection (t-self._multiplierIndex-1,1)
|
||||
|
||||
def text(self):
|
||||
return self.entry.text()
|
||||
|
||||
def setText(self, text):
|
||||
self.entry.setText(text)
|
||||
self._value = int(text)
|
||||
|
||||
def value(self):
|
||||
return int(self.entry.text())
|
||||
|
||||
def setValue(self, value):
|
||||
self._value = int(value)
|
||||
self.entry.setText(str(self._value))
|
||||
|
||||
# class patch or should we call a dialog from here too?
|
||||
def callDialog(self,widget):
|
||||
pass
|
||||
|
||||
class TouchDoubleSpinBox(TouchSpinBox):
|
||||
def __init__(self, parent=None):
|
||||
super(TouchDoubleSpinBox, self).__init__(parent)
|
||||
|
||||
def init_settings(self):
|
||||
self._value = 10.001
|
||||
self.validDouble = QDoubleValidator(-999.999, 999.999, 3)
|
||||
self.entry.setText(str('10.001'))
|
||||
self.entry.setValidator(self.validDouble)
|
||||
self.entry.returnPressed.connect(lambda :self.entryUpdate())
|
||||
|
||||
def updateValue(self, state):
|
||||
def convert(x):
|
||||
return ['0','1','2','3','4','5','6','7','8','9','-',' ','end'][x]
|
||||
def indexof(x):
|
||||
return ['0','1','2','3','4','5','6','7','8','9','-',' ','end'].index(x)
|
||||
if self.entry.text() == '':
|
||||
self.entry.setText('0')
|
||||
pos = self.getPosition(self._multiplierIndex)
|
||||
x = self.entry.text()[pos]
|
||||
print('update txt:',x,'pos',pos)
|
||||
if state:
|
||||
next = convert(indexof(x)+1)
|
||||
if next in( '-',' ','end'):
|
||||
if pos == 0:
|
||||
if next != 'end':
|
||||
pass
|
||||
else:
|
||||
next = 0
|
||||
else:
|
||||
next = 0
|
||||
else:
|
||||
next = convert(indexof(x)-1)
|
||||
if next in( '-',' ','end'):
|
||||
if pos == 0:
|
||||
if next != 'end':
|
||||
pass
|
||||
else:
|
||||
next = 0
|
||||
else:
|
||||
next = 0
|
||||
self.entry.setText( self.replace_at_index(self.entry.text(),pos,str(next)))
|
||||
self.entry.setSelection (pos,1)
|
||||
|
||||
def replace_at_index(self,text,index=0,replacement=''):
|
||||
return '%s%s%s'%(text[:index],replacement,text[index+1:])
|
||||
|
||||
def getPosition(self, index):
|
||||
t = len(self.entry.text())-1
|
||||
print('T:',t,'Index:',index)
|
||||
if t < 0:t=0
|
||||
if index > t:
|
||||
self.entry.setText('0'+ self.entry.text())
|
||||
t = len(self.entry.text())-1
|
||||
#index = 0
|
||||
|
||||
if self.entry.text()[t-index] in ('.',','):
|
||||
index+=1
|
||||
pos = t-index
|
||||
self._multiplierIndex = index
|
||||
return pos
|
||||
|
||||
def moveLeft(self):
|
||||
index = self._multiplierIndex +1
|
||||
t = len(self.entry.text())-1
|
||||
print('T:',t,'Index:',index)
|
||||
if t < 0:t=0
|
||||
if index == -1:
|
||||
if '.' in self.entry.text():
|
||||
self.entry.setText(self.entry.text()+'0')
|
||||
t = len(self.entry.text())-1
|
||||
index = 0
|
||||
else:
|
||||
self.entry.setText(self.entry.text()+'.0')
|
||||
t = len(self.entry.text())-1
|
||||
index = 0
|
||||
|
||||
|
||||
if self.entry.text()[t-index] in ('.',','):
|
||||
index-=1
|
||||
pos = t-index
|
||||
self._multiplierIndex = index
|
||||
|
||||
try:
|
||||
print('text;',self.entry.text()[pos])
|
||||
except:
|
||||
pass
|
||||
#self.select.setText(str(pos))
|
||||
self.entry.setSelection (pos,1)
|
||||
|
||||
def moveRight(self):
|
||||
index = self._multiplierIndex -1
|
||||
t = len(self.entry.text())-1
|
||||
print('T:',t,'Index:',index)
|
||||
if t < 0:t=0
|
||||
if index == -1:
|
||||
if '.' in self.entry.text():
|
||||
self.entry.setText(self.entry.text()+'0')
|
||||
t = len(self.entry.text())-1
|
||||
index = 0
|
||||
else:
|
||||
self.entry.setText(self.entry.text()+'.0')
|
||||
t = len(self.entry.text())-1
|
||||
index = 0
|
||||
|
||||
|
||||
if self.entry.text()[t-index] in ('.',','):
|
||||
index-=1
|
||||
pos = t-index
|
||||
self._multiplierIndex = index
|
||||
|
||||
try:
|
||||
print('text;',self.entry.text()[pos])
|
||||
except:
|
||||
pass
|
||||
#self.select.setText(str(pos))
|
||||
self.entry.setSelection (pos,1)
|
||||
|
||||
def moveLeft(self):
|
||||
pos = self.getPosition(self._multiplierIndex +1)
|
||||
|
||||
try:
|
||||
print('text;',self.entry.text()[pos])
|
||||
except:
|
||||
pass
|
||||
|
||||
#self.select.setText(str(pos))
|
||||
self.entry.setSelection (pos,1)
|
||||
|
||||
def setValue(self, value):
|
||||
self.entry.setText('{:4.3f}'.format(value))
|
||||
|
||||
def setText(self, text):
|
||||
self.entry.setText('{:4.3f}'.format(float(text)))
|
||||
|
||||
def entryUpdate(self):
|
||||
print('Update:',self.entry.text())
|
||||
if self.entry.text() == '':
|
||||
self.setValue(0)
|
||||
|
||||
# for direct testing
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = TouchSpinBox()
|
||||
w.show()
|
||||
w = TouchDoubleSpinBox()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
298
lib/python/qtvcp/widgets/user_dro_label.py
Normal file
@@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
# QtVcp Widget - user temporary DRO label widget
|
||||
# This widgets displays linuxcnc axis position information.
|
||||
#
|
||||
# Copyright (c) 2025 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 linuxcnc
|
||||
from math import isclose
|
||||
|
||||
from qtpy import QtCore
|
||||
from qtpy.QtWidgets import QMenu, QAction
|
||||
from qtpy.QtGui import QIcon, QColor
|
||||
|
||||
from qtvcp.widgets.simple_widgets import ScaledLabel
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Info, Action
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# INFO holds INI file details
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
INFO = Info()
|
||||
ACTION = Action()
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
# LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
|
||||
class UserDROLabel(ScaledLabel, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(UserDROLabel, self).__init__(parent)
|
||||
# axis index of 9 axis
|
||||
self.axis_index = 0
|
||||
# linuxcnc joint number
|
||||
self._joint_type = 1
|
||||
self.display_units_mm = 0
|
||||
self.metric_text_template = '%10.3f'
|
||||
self.imperial_text_template = '%9.4f'
|
||||
self.angular_text_template = '%9.2f'
|
||||
self._text =' -000.0000'
|
||||
self._offset = [0.0] * 9
|
||||
self._lastOffset = [0.0] * 9
|
||||
self._negativeColor = QColor(0, 0, 100, 150)
|
||||
self._positiveColor = QColor(100, 0, 0, 150)
|
||||
self._zeroColor = QColor(0, 100, 0, 150)
|
||||
self._imperialTolerence = .00005
|
||||
self._metricTolerence = .005
|
||||
self._currentTolerence = .0005
|
||||
self._lastNum = 0.0
|
||||
|
||||
# menu stuff
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
SettingMenu = QMenu(self)
|
||||
self.settingMenu = SettingMenu
|
||||
self.zeroButton = QAction(QIcon('exit24.png'), 'Zero', self)
|
||||
self.zeroButton.triggered.connect(self.Zero)
|
||||
SettingMenu.addAction(self.zeroButton)
|
||||
self.setButton = QAction(QIcon('exit24.png'), 'Set', self)
|
||||
self.setButton.triggered.connect(self.SetOrigin)
|
||||
SettingMenu.addAction(self.setButton)
|
||||
self.lastButton = QAction(QIcon('exit24.png'), 'Set To Last', self)
|
||||
self.lastButton.triggered.connect(self.Last)
|
||||
SettingMenu.addAction(self.lastButton)
|
||||
|
||||
SettingMenu.popup(event.globalPos())
|
||||
|
||||
def _hal_init(self):
|
||||
super(UserDROLabel, self)._hal_init()
|
||||
STATUS.connect('current-position', self.update)
|
||||
STATUS.connect('metric-mode-changed', self._switch_units)
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
elif self.display_units_mm:
|
||||
self._current_text_template = self.metric_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
self._currentTolerence = self._metricTolerence
|
||||
else:
|
||||
self._currentTolerence = self._imperialTolerence
|
||||
|
||||
def update(self, widget, absolute, relative, dtg, joint):
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
absolute = INFO.convert_units_9(absolute)
|
||||
|
||||
tmpl = lambda s: self._current_text_template % s
|
||||
|
||||
try:
|
||||
num = absolute[self.axis_index] + self._offset[self.axis_index]
|
||||
self.setColor(num)
|
||||
self.setText(tmpl(num))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def setColor(self, data):
|
||||
if not data == self._lastNum:
|
||||
if isclose(data, 0,abs_tol = self._currentTolerence) :
|
||||
self.setStyleSheet("QLabel { background-color :"+self._zeroColor.name()+" ;}");
|
||||
elif data <0:
|
||||
self.setStyleSheet("QLabel { background-color :"+self._negativeColor.name()+" ;}");
|
||||
else:
|
||||
self.setStyleSheet("QLabel { background-color :"+self._positiveColor.name()+" ;}");
|
||||
self._lastNum = data
|
||||
|
||||
def _switch_units(self, widget, data):
|
||||
self.display_units_mm = data
|
||||
self.update_units()
|
||||
|
||||
def update_units(self):
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
elif self.display_units_mm:
|
||||
self._current_text_template = self.metric_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
def set_to_inch(self):
|
||||
self.display_units_mm = 0
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
else:
|
||||
self._current_text_template = self.imperial_text_template
|
||||
|
||||
def set_to_mm(self):
|
||||
self.display_units_mm = 1
|
||||
if self._joint_type == linuxcnc.ANGULAR:
|
||||
self._current_text_template = self.angular_text_template
|
||||
else:
|
||||
self._current_text_template = self.metric_text_template
|
||||
|
||||
def get_current_position(self):
|
||||
if STATUS.is_joint_mode():
|
||||
return None
|
||||
p,r,d = STATUS.get_position()
|
||||
if self.display_units_mm != INFO.MACHINE_IS_METRIC:
|
||||
p = INFO.convert_units_9(p)
|
||||
return p[self.axis_index]
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def zero (self, data=True):
|
||||
p = self.get_current_position()
|
||||
self._lastOffset[self.axis_index] = self._offset[self.axis_index]
|
||||
self._offset[self.axis_index] = - p
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def setAxisIndex(self, num):
|
||||
self.axis_index = num
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def setAxisLetter(self, let):
|
||||
self.axis_index = self.axisToIndex(let)
|
||||
print(let,self.axis_index)
|
||||
|
||||
def axisToIndex(self, let):
|
||||
conversion = {"X":0, "Y":1, "Z":2, "A":3, "B":4, "C":5, "U":6, "V":7, "W":8}
|
||||
return conversion[let]
|
||||
|
||||
def indexToAxis(self, index):
|
||||
conversion = {0:"X", 1:"Y", 2:"Z", 3:"A", 4:"B", 5:"C", 6:"U", 7:"V", 8:"W"}
|
||||
return conversion[index]
|
||||
|
||||
#####################
|
||||
# Menu callback code
|
||||
#####################
|
||||
def Zero(self):
|
||||
self.zero()
|
||||
|
||||
def SetOrigin(self):
|
||||
now = self.get_current_position()
|
||||
axis = self.indexToAxis(self.axis_index)
|
||||
if axis:
|
||||
mess = {'NAME':self.dialog_code,'ID':'%s__' % self.objectName(),
|
||||
'AXIS':axis,'CURRENT':now,
|
||||
'TITLE':'Set %s Reference'% axis,
|
||||
'GEONAME':'UserDROLabelDialog_{}'.format(self.dialog_code),
|
||||
'AXIS':axis}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
def Last(self):
|
||||
axis = self.indexToAxis(self.axis_index)
|
||||
now = self.get_current_position() + self._offset[self.axis_index]
|
||||
if axis:
|
||||
temp = self._offset[self.axis_index]
|
||||
self._offset[self.axis_index] = self._lastOffset[self.axis_index]
|
||||
self._lastOffset[self.axis_index] = temp
|
||||
text = 'Reset Axis %s from %f to Last Value: %f' %(axis, now, self._lastOffset[self.axis_index] )
|
||||
|
||||
# process the STATUS return message
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
if code and name and num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
axis = message.get('AXIS')
|
||||
# do something
|
||||
|
||||
p = self.get_current_position()
|
||||
self._lastOffset[self.axis_index] = self._offset[self.axis_index]
|
||||
self._offset[self.axis_index] = - (p-num)
|
||||
|
||||
#########################################################################
|
||||
# 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 setmetrictemplate(self, data):
|
||||
self.metric_text_template = data
|
||||
self.update_units()
|
||||
def getmetrictemplate(self):
|
||||
return self.metric_text_template
|
||||
def resetmetrictemplate(self):
|
||||
self.metric_text_template = '%10.3f'
|
||||
metric_template = QtCore.Property(str, getmetrictemplate, setmetrictemplate, resetmetrictemplate)
|
||||
|
||||
def setimperialtexttemplate(self, data):
|
||||
self.imperial_text_template = data
|
||||
self.update_units()
|
||||
def getimperialtexttemplate(self):
|
||||
return self.imperial_text_template
|
||||
def resetimperialtexttemplate(self):
|
||||
self.imperial_text_template = '%9.4f'
|
||||
imperial_template = QtCore.Property(str, getimperialtexttemplate, setimperialtexttemplate, resetimperialtexttemplate)
|
||||
|
||||
def setangulartexttemplate(self, data):
|
||||
self.angular_text_template = data
|
||||
self.update_units()
|
||||
def getangulartexttemplate(self):
|
||||
return self.angular_text_template
|
||||
def resetangulartexttemplate(self):
|
||||
self.angular_text_template = '%9.2f'
|
||||
angularTemplate = QtCore.Property(str, getangulartexttemplate, setangulartexttemplate, resetangulartexttemplate)
|
||||
|
||||
def getNegativeColor(self):
|
||||
return self._negativeColor
|
||||
def setNegativeColor(self, value):
|
||||
self._negativeColor = value
|
||||
negativeColor = QtCore.Property(QColor, getNegativeColor, setNegativeColor)
|
||||
|
||||
def getPositiveColor(self):
|
||||
return self._positiveColor
|
||||
def setPositiveColor(self, value):
|
||||
self._positiveColor = value
|
||||
positiveColor = QtCore.Property(QColor, getPositiveColor, setPositiveColor)
|
||||
|
||||
####################
|
||||
## menu properties
|
||||
####################
|
||||
|
||||
def set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialogName = QtCore.Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
##############################
|
||||
# required class boiler code #
|
||||
##############################
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
# for testing without editor:
|
||||
def main():
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = UserDROLabel()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
795
lib/python/qtvcp/widgets/versa_probe.py
Normal file
@@ -0,0 +1,795 @@
|
||||
#!/usr/bin/env python3
|
||||
# Qtvcp versa probe
|
||||
#
|
||||
# Copyright (c) 2018 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# a probe screen based on Versa probe screen
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hal
|
||||
import json
|
||||
import re
|
||||
|
||||
from qtpy import QtGui, QtCore, QtWidgets, uic
|
||||
from qtpy.QtCore import QProcess, QEvent, Qt, Property
|
||||
from qtpy.QtWidgets import QDialogButtonBox, QAbstractSlider
|
||||
|
||||
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
||||
from qtvcp.core import Status, Action, Info, Path
|
||||
from qtvcp.widgets.dialogMixin import GeometryMixin
|
||||
from qtvcp import logger
|
||||
# Instantiate the libraries with global reference
|
||||
# STATUS gives us status messages from linuxcnc
|
||||
# LOG is for running code logging
|
||||
STATUS = Status()
|
||||
ACTION = Action()
|
||||
INFO = Info()
|
||||
PATH = Path()
|
||||
LOG = logger.getLogger(__name__)
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
current_dir = os.path.dirname(__file__)
|
||||
SUBPROGRAM = os.path.abspath(os.path.join(current_dir, 'probe_subprog.py'))
|
||||
|
||||
# can use/favours local image and help files
|
||||
HELP = PATH.find_widget_path()
|
||||
ICONPATH = os.path.join(PATH.find_image_path(), 'probe_icons')
|
||||
|
||||
DEFAULT = 0
|
||||
WARNING = 1
|
||||
CRITICAL = 2
|
||||
|
||||
class VersaProbeParent(QtWidgets.QWidget, _HalWidgetBase):
|
||||
def __init__(self, parent=None):
|
||||
super(VersaProbeParent, self).__init__(parent)
|
||||
self.proc = None
|
||||
self.tool_diameter = None
|
||||
self.tool_number = None
|
||||
self.probe_number = -1
|
||||
self._nextIndex = 0
|
||||
self._cmd = None
|
||||
self._runImmediately = True
|
||||
|
||||
STATUS.connect('tool-info-changed', lambda w, data: self._tool_info(data))
|
||||
if INFO.MACHINE_IS_METRIC:
|
||||
self.valid = QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r'^((\d{1,4}(\.\d{1,3})?)|(\.\d{1,3}))$'))
|
||||
else:
|
||||
self.valid = QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r'^((\d{1,3}(\.\d{1,4})?)|(\.\d{1,4}))$'))
|
||||
self.setMinimumSize(600, 420)
|
||||
# Load the widgets UI file will use local file if available:
|
||||
self.filename = PATH.find_widget_path('versa_probe.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
self.hilightStyle = "border: 2px solid red;"
|
||||
|
||||
#create parameter dictionary
|
||||
self.send_dict = {}
|
||||
# these parameters are sent to the subprogram
|
||||
# the is also the order of the next widget when dialog 'next' button is pressed
|
||||
self.parm_list = ['probe_diam', 'rapid_vel', 'search_vel', 'probe_vel',
|
||||
'xy_clearance', 'z_clearance', 'side_edge_length',
|
||||
'max_travel', 'max_z_travel', 'latch_return_dist',
|
||||
'tool_probe_height', 'tool_block_height',
|
||||
'adj_x', 'adj_y', 'adj_z', 'adj_angle',]
|
||||
self.status_list = ['xm', 'xc', 'xp', 'ym', 'yc', 'yp', 'lx', 'ly', 'z', 'd', 'a','th','bh']
|
||||
|
||||
for i in self.parm_list:
|
||||
self['input_' + i].setValidator(self.valid)
|
||||
# button connections
|
||||
self.btn_help.clicked.connect(self.help_clicked)
|
||||
self.inside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.outside_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.skew_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.length_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.tool_buttonGroup.buttonClicked.connect(self.probe_btn_clicked)
|
||||
self.pbtn_set_x.released.connect(self.pbtn_set_x_released)
|
||||
self.pbtn_set_y.released.connect(self.pbtn_set_y_released)
|
||||
self.pbtn_set_z.released.connect(self.pbtn_set_z_released)
|
||||
self.pbtn_set_angle.released.connect(self.pbtn_set_angle_released)
|
||||
|
||||
self.buildToolTip(self.input_search_vel, 'Search Velocity', 'search_vel')
|
||||
self.buildToolTip(self.input_probe_vel, 'Probe Velocity', 'probe_vel')
|
||||
self.buildToolTip(self.input_z_clearance, 'Z Clearence Distance', 'Zclearance')
|
||||
self.buildToolTip(self.input_max_travel, 'Maximum Probe Search Distance', 'rapid')
|
||||
self.buildToolTip(self.input_latch_return_dist, 'Return After Latch Distance', 'rapid_return')
|
||||
self.buildToolTip(self.input_probe_diam,'Probe Diameter','probe_diam')
|
||||
self.buildToolTip(self.input_xy_clearance, 'XY Clearence Distance', 'XYclearance')
|
||||
self.buildToolTip(self.input_side_edge_length, 'Edge length', 'edge_length')
|
||||
#self.buildToolTip(self.input_tool_probe_height, '', '')
|
||||
#self.buildToolTip(self.input_tool_block_height, '', '')
|
||||
#self.buildToolTip(self.input_adj_x, '', '')
|
||||
#self.buildToolTip(self.input_adj_y, '', '')
|
||||
#self.buildToolTip(self.input_adj_z, '', '')
|
||||
#self.buildToolTip(self.input_adj_angle, '', '')
|
||||
self.buildToolTip(self.input_rapid_vel, 'Rapid Velocity', 'rapid_vel')
|
||||
|
||||
# catch focusIn event to pop calculator dialog
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.FocusIn:
|
||||
if isinstance(obj, QtWidgets.QLineEdit):
|
||||
# only if mouse selected
|
||||
if event.reason () == 0:
|
||||
self._nextIndex = self.parm_list.index(obj.objectName().replace('input_',''))
|
||||
self.popEntry(obj)
|
||||
if self.dialog_code == 'CALCULATOR':
|
||||
obj.clearFocus()
|
||||
event.accept()
|
||||
return True
|
||||
return super(VersaProbeParent, self).eventFilter(obj, event)
|
||||
|
||||
# keep track of tool number and diameter
|
||||
# update the probe loaded HAL pin
|
||||
# can be used to inhibit the spindle
|
||||
def _tool_info(self, data):
|
||||
if data.id != -1:
|
||||
self.tool_diameter = data.diameter
|
||||
self.tool_number = data.id
|
||||
if self.probe_number == self.tool_number:
|
||||
self.probe_loaded.set(True)
|
||||
else:
|
||||
self.probe_loaded.set(False)
|
||||
return
|
||||
self.tool_diameter = None
|
||||
self.tool_number = None
|
||||
self.probe_loaded.set(False)
|
||||
|
||||
def _hal_init(self):
|
||||
def homed_on_test():
|
||||
return (STATUS.machine_is_on() and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))
|
||||
|
||||
# get current style of the line input
|
||||
self._oldstyle = self.input_search_vel.styleSheet()
|
||||
|
||||
# have to call hal_init on widgets in this widget ourselves
|
||||
# qtvcp doesn't see them otherwise
|
||||
oldname = self.HAL_GCOMP_.comp.getprefix()
|
||||
self.HAL_GCOMP_.comp.setprefix('qtversaprobe')
|
||||
self.pbtn_use_tool_measurement.setProperty('pin_name','enable')
|
||||
self.pbtn_use_tool_measurement.hal_init()
|
||||
self.HAL_GCOMP_.comp.setprefix(oldname)
|
||||
|
||||
self.allow_auto_skew.hal_init()
|
||||
self.allow_auto_zero.hal_init()
|
||||
self.statuslabel_motiontype.hal_init()
|
||||
self.statelabel_machineUnits.hal_init()
|
||||
self.statelabel_machineUnits_2.hal_init()
|
||||
self.help = HelpDialog(self.QTVCP_INSTANCE_)
|
||||
self.help.hal_init(HAL_NAME='_versa_help')
|
||||
|
||||
# connect to STATUS
|
||||
STATUS.connect('state-off', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('state-estop', lambda w: self.setEnabled(False))
|
||||
STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_test()))
|
||||
STATUS.connect('all-homed', lambda w: self.setEnabled(homed_on_test()))
|
||||
# STATUS.connect('error', self.send_error)
|
||||
STATUS.connect('periodic', lambda w: self.check_probe())
|
||||
STATUS.connect('general',self.return_value)
|
||||
|
||||
# install event filters on all the lineedits
|
||||
# so we can call up a dialog when lineedit get focus
|
||||
self.input_search_vel.installEventFilter(self)
|
||||
self.input_probe_vel.installEventFilter(self)
|
||||
self.input_z_clearance.installEventFilter(self)
|
||||
self.input_max_travel.installEventFilter(self)
|
||||
self.input_max_z_travel.installEventFilter(self)
|
||||
self.input_latch_return_dist.installEventFilter(self)
|
||||
self.input_probe_diam.installEventFilter(self)
|
||||
self.input_xy_clearance.installEventFilter(self)
|
||||
self.input_side_edge_length.installEventFilter(self)
|
||||
self.input_tool_probe_height.installEventFilter(self)
|
||||
self.input_tool_block_height.installEventFilter(self)
|
||||
self.input_adj_x.installEventFilter(self)
|
||||
self.input_adj_y.installEventFilter(self)
|
||||
self.input_adj_z.installEventFilter(self)
|
||||
self.input_adj_angle.installEventFilter(self)
|
||||
self.input_rapid_vel.installEventFilter(self)
|
||||
|
||||
self.set_checkableButtons(not self._runImmediately)
|
||||
|
||||
if self.PREFS_:
|
||||
self.probe_number = self.PREFS_.getpref('ps_probe_tool', -1, int, 'VERSA_PROBE_OPTIONS')
|
||||
self.input_search_vel.setText(str(self.PREFS_.getpref( "ps_searchvel", 300.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_probe_vel.setText(str(self.PREFS_.getpref( "ps_probevel", 10.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_z_clearance.setText(str(self.PREFS_.getpref( "ps_z_clearance", 3.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_max_travel.setText(str(self.PREFS_.getpref( "ps_probe_max", 1.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_max_z_travel.setText(str(self.PREFS_.getpref( "ps_probe_max_z_travel", 1.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_latch_return_dist.setText(str(self.PREFS_.getpref( "ps_probe_latch", 0.5, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_probe_diam.setText(str(self.PREFS_.getpref( "ps_probe_diam", 2.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_xy_clearance.setText(str(self.PREFS_.getpref( "ps_xy_clearance", 5.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_side_edge_length.setText(str(self.PREFS_.getpref( "ps_side_edge_length", 5.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_tool_probe_height.setText(str(self.PREFS_.getpref( "ps_probe_height", 20.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_tool_block_height.setText(str(self.PREFS_.getpref( "ps_block_height", 20.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_adj_x.setText(str(self.PREFS_.getpref( "ps_offs_x", 0.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_adj_y.setText(str(self.PREFS_.getpref( "ps_offs_y", 0.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_adj_z.setText(str(self.PREFS_.getpref( "ps_offs_z", 0.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_adj_angle.setText(str(self.PREFS_.getpref( "ps_offs_angle", 0.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.input_rapid_vel.setText(str(self.PREFS_.getpref( "ps_probe_rapid_vel", 60.0, float, 'VERSA_PROBE_OPTIONS')) )
|
||||
|
||||
self.pbtn_use_tool_measurement.setChecked((self.PREFS_.getpref( "use_tool_measurement", True, bool, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.allow_auto_zero.setChecked((self.PREFS_.getpref( "use_auto_zero", True, bool, 'VERSA_PROBE_OPTIONS')) )
|
||||
self.allow_auto_skew.setChecked((self.PREFS_.getpref( "use_auto_skew", True, bool, 'VERSA_PROBE_OPTIONS')) )
|
||||
|
||||
self.z_max_clear = INFO.get_safe_float("VERSA_TOOLSETTER", "Z_MAX_CLEAR")
|
||||
self.ts_x = INFO.get_safe_float('VERSA_TOOLSETTER','X')
|
||||
self.ts_y = INFO.get_safe_float('VERSA_TOOLSETTER','Y')
|
||||
self.ts_z = INFO.get_safe_float('VERSA_TOOLSETTER','Z')
|
||||
self.ts_max = INFO.get_safe_float('VERSA_TOOLSETTER','MAXPROBE')
|
||||
self.ts_diam = INFO.get_safe_float('VERSA_TOOLSETTER','DIAMETER')
|
||||
|
||||
# make pins available for tool measure remaps
|
||||
oldname = self.HAL_GCOMP_.comp.getprefix()
|
||||
self.HAL_GCOMP_.comp.setprefix('qtversaprobe')
|
||||
self.pin_svel = self.HAL_GCOMP_.newpin("searchvel", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.pin_svel.set(float(self.input_search_vel.text()))
|
||||
self.pin_pvel = self.HAL_GCOMP_.newpin("probevel", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.pin_pvel.set(float(self.input_probe_vel.text()))
|
||||
self.pin_pheight = self.HAL_GCOMP_.newpin("probeheight", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.pin_pheight.set(float(self.input_tool_probe_height.text()))
|
||||
self.pin_bheight = self.HAL_GCOMP_.newpin("blockheight", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.pin_bheight.set(float(self.input_tool_block_height.text()))
|
||||
self.pin_latch_rtn = self.HAL_GCOMP_.newpin("backoffdist", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.pin_latch_rtn.set(float(self.input_latch_return_dist.text()))
|
||||
self.probe_loaded = self.HAL_GCOMP_.newpin("probe-loaded", hal.HAL_BIT, hal.HAL_OUT)
|
||||
self.HAL_GCOMP_.comp.setprefix(oldname)
|
||||
|
||||
# install callbacks to update HAL pins
|
||||
self.input_search_vel.textChanged.connect(self.update_search_vel_pin)
|
||||
self.input_probe_vel.textChanged.connect(self.update_probe_vel_pin)
|
||||
self.input_tool_probe_height.textChanged.connect(self.update_probe_height_pin)
|
||||
self.input_tool_block_height.textChanged.connect(self.update_block_height_pin)
|
||||
self.input_latch_return_dist.textChanged.connect(self.update_latch_return_dist_pin)
|
||||
|
||||
# when qtvcp closes this gets called
|
||||
def _hal_cleanup(self):
|
||||
if self.PREFS_:
|
||||
LOG.debug('Saving Versa probe data to preference file.')
|
||||
self.PREFS_.putpref('ps_probe_tool', self.probe_number, int, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_searchvel", float(self.input_search_vel.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probevel", float(self.input_probe_vel.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_z_clearance", float(self.input_z_clearance.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_max", float(self.input_max_travel.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_max_z_travel", float(self.input_max_z_travel.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_latch", float(self.input_latch_return_dist.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_diam", float(self.input_probe_diam.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_xy_clearance", float(self.input_xy_clearance.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_side_edge_length", float(self.input_side_edge_length.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_height", float(self.input_tool_probe_height.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_block_height", float(self.input_tool_block_height.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_offs_x", float(self.input_adj_x.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_offs_y", float(self.input_adj_y.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_offs_z", float(self.input_adj_z.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_offs_angle", float(self.input_adj_angle.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "ps_probe_rapid_vel", float(self.input_rapid_vel.text()), float, 'VERSA_PROBE_OPTIONS')
|
||||
|
||||
self.PREFS_.putpref( "use_tool_measurement", bool(self.pbtn_use_tool_measurement.isChecked()), bool, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "use_auto_zero", bool(self.allow_auto_zero.isChecked()), bool, 'VERSA_PROBE_OPTIONS')
|
||||
self.PREFS_.putpref( "use_auto_skew", bool(self.allow_auto_skew.isChecked()), bool, 'VERSA_PROBE_OPTIONS')
|
||||
|
||||
# process the STATUS return message
|
||||
# set the line edit to the value if not cancelled
|
||||
def return_value(self, w, message):
|
||||
num = message['RETURN']
|
||||
code = bool(message.get('ID') == '%s__'% self.objectName())
|
||||
name = bool(message.get('NAME') == self.dialog_code)
|
||||
next = message.get('NEXT', False)
|
||||
back = message.get('BACK', False)
|
||||
if code and name:
|
||||
obj = message.get('OBJECT')
|
||||
if num is not None:
|
||||
LOG.debug('message return:{}'.format (message))
|
||||
obj.setText(str(num))
|
||||
# clear high lighting
|
||||
obj.setStyleSheet(self._oldstyle)
|
||||
# request for next input widget from parm_list
|
||||
if next:
|
||||
self._nextIndex += 1
|
||||
if self._nextIndex == len(self.parm_list):
|
||||
self._nextIndex = 0
|
||||
newobj = self['input_{}'.format(self.parm_list[self._nextIndex])]
|
||||
# update the dialog
|
||||
self.popEntry(newobj,True)
|
||||
elif back:
|
||||
self._nextIndex -= 1
|
||||
if self._nextIndex == -1:
|
||||
self._nextIndex = len(self.parm_list)-1
|
||||
newobj = self['input_{}'.format(self.parm_list[self._nextIndex])]
|
||||
# update the dialog
|
||||
self.popEntry(newobj,True)
|
||||
|
||||
|
||||
def popEntry(self, obj, next=False):
|
||||
obj.setStyleSheet(self.hilightStyle)
|
||||
mess = {'NAME':self.dialog_code,
|
||||
'ID':'%s__' % self.objectName(),
|
||||
'OVERLAY':False,
|
||||
'OBJECT':obj,
|
||||
'TITLE':'Set Entry for {}'.format(obj.objectName().upper()),
|
||||
'GEONAME':'_{}'.format(self.dialog_code),
|
||||
'NEXT':next,
|
||||
'WIDGETCYCLE': True
|
||||
}
|
||||
STATUS.emit('dialog-request', mess)
|
||||
LOG.debug('message sent:{}'.format (mess))
|
||||
|
||||
def buildToolTip(self,obj, text, icon):
|
||||
path = os.path.join(ICONPATH, icon)
|
||||
obj.setToolTip('''<html><head/><body><p align="center">
|
||||
<span style=" font-weight:600;">{}</span></p><p align="center">
|
||||
<img src="{}"/></p></body></html>''' .format(text, path))
|
||||
|
||||
#############################################
|
||||
# process control
|
||||
#############################################
|
||||
def start_process(self):
|
||||
self.proc = QProcess()
|
||||
self.proc.setReadChannel(QProcess.StandardOutput)
|
||||
self.proc.started.connect(self.process_started)
|
||||
self.proc.readyReadStandardOutput.connect(self.read_stdout)
|
||||
self.proc.readyReadStandardError.connect(self.read_stderror)
|
||||
self.proc.finished.connect(self.process_finished)
|
||||
self.proc.start('python3 {}'.format(SUBPROGRAM))
|
||||
|
||||
def start_probe(self, cmd):
|
||||
if self.proc is not None:
|
||||
LOG.info("Probe Routine processor is busy")
|
||||
return
|
||||
ACTION.RECORD_CURRENT_MODE()
|
||||
self.start_process()
|
||||
string_to_send = cmd + '$' + json.dumps(self.send_dict) + '\n'
|
||||
#print("String to send ", string_to_send)
|
||||
STATUS.block_error_polling()
|
||||
self.proc.writeData(bytes(string_to_send, 'utf-8'))
|
||||
|
||||
def process_started(self):
|
||||
LOG.info("Versa_Probe started with PID {}\n".format(self.proc.processId()))
|
||||
|
||||
def read_stdout(self):
|
||||
qba = self.proc.readAllStandardOutput()
|
||||
line = qba.data()
|
||||
self.parse_input(line)
|
||||
|
||||
def read_stderror(self):
|
||||
qba = self.proc.readAllStandardError()
|
||||
line = qba.data()
|
||||
self.parse_input(line)
|
||||
|
||||
def process_finished(self, exitCode, exitStatus):
|
||||
LOG.info(("Probe Process finished - exitCode {} exitStatus {}".format(exitCode, exitStatus)))
|
||||
self.proc = None
|
||||
STATUS.unblock_error_polling()
|
||||
ACTION.RESTORE_RECORDED_MODE()
|
||||
|
||||
def parse_input(self, line):
|
||||
line = line.decode("utf-8")
|
||||
if "ERROR INFO" in line:
|
||||
ACTION.SET_ERROR_MESSAGE(line)
|
||||
elif "ERROR" in line:
|
||||
#print(line)
|
||||
STATUS.unblock_error_polling()
|
||||
ACTION.SET_ERROR_MESSAGE('Versa Probe process finished in error')
|
||||
elif "PROBE_ROUTINES" in line:
|
||||
if LOG.getEffectiveLevel() < logger.INFO:
|
||||
print(line)
|
||||
elif "INFO" in line:
|
||||
pass
|
||||
elif "COMPLETE" in line:
|
||||
STATUS.unblock_error_polling()
|
||||
LOG.info("Versa Probing routine completed without errors")
|
||||
return_data = line.rstrip().split('$')
|
||||
data = json.loads(return_data[1])
|
||||
self.show_results(data)
|
||||
elif "HISTORY" in line:
|
||||
if not self.set_statusbar(line, DEFAULT):
|
||||
STATUS.emit('update-machine-log', line, 'TIME')
|
||||
elif "DEBUG" in line:
|
||||
pass
|
||||
else:
|
||||
LOG.error("Error parsing return data from sub_processor. Line={}".format(line))
|
||||
|
||||
#####################################################
|
||||
# button callbacks
|
||||
#####################################################
|
||||
|
||||
def help_clicked(self):
|
||||
self.pop_help()
|
||||
|
||||
def probe_btn_clicked(self, button):
|
||||
cmd = button
|
||||
#print("Button clicked ", cmd.property('probe'),self._cmd)
|
||||
|
||||
# run probe when buttons are pressed
|
||||
if self._runImmediately:
|
||||
self.set_statusbar('Versa Probe: Start Cycle: {}'.format(cmd.toolTip(),DEFAULT))
|
||||
self.get_parms()
|
||||
self.start_probe(cmd.property('probe'))
|
||||
|
||||
# select probe when buttons pressed, run when start_cycle function called
|
||||
else:
|
||||
# Auto exclusive doesn't allow unchecking all buttons
|
||||
# We force it here
|
||||
if cmd == self._cmd:
|
||||
button.group().setExclusive(False)
|
||||
button.blockSignals(True)
|
||||
button.setChecked(False)
|
||||
button.blockSignals(False)
|
||||
button.group().setExclusive(True)
|
||||
self._cmd = None
|
||||
return
|
||||
self.set_statusbar('Versa probe: Selected: {}'.format(cmd.toolTip()),DEFAULT,noLog=True)
|
||||
self._cmd = cmd
|
||||
|
||||
# called externally to run selected routine
|
||||
# if not in _runImmediately mode
|
||||
def cycle_start(self):
|
||||
if self._runImmediately:
|
||||
self.set_statusbar('Versa Probe set to run buttons immediately',WARNING)
|
||||
return
|
||||
if self._cmd is None:
|
||||
self.set_statusbar('No Versa Probe probe selected',WARNING)
|
||||
return
|
||||
self.set_statusbar('Versa Probe: Start Cycle: {}'.format(self._cmd.toolTip(),DEFAULT))
|
||||
self.get_parms()
|
||||
self.start_probe(self._cmd.property('probe'))
|
||||
|
||||
###### set origin offset ######################
|
||||
def pbtn_set_x_released(self):
|
||||
ACTION.SET_AXIS_ORIGIN('X', float(self.input_adj_x.text()))
|
||||
|
||||
def pbtn_set_y_released(self):
|
||||
ACTION.SET_AXIS_ORIGIN('Y', float(self.input_adj_y.text()))
|
||||
|
||||
def pbtn_set_z_released(self):
|
||||
ACTION.SET_AXIS_ORIGIN('Z', float(self.input_adj_z.text()))
|
||||
|
||||
def pbtn_set_angle_released(self):
|
||||
self.status_a = "%.3f" % float(self.input_adj_angle.text())
|
||||
s="G10 L2 P0"
|
||||
if self.allow_auto_zero.isChecked():
|
||||
s += " X%.4f"% float(self.input_adj_x.text())
|
||||
s += " Y%.4f"% float(self.input_adj_y.text())
|
||||
else :
|
||||
a = STATUS.get_probed_position_with_offsets()
|
||||
s += " X%.4f"%a[0]
|
||||
s += " Y%.4f"%a[1]
|
||||
s += " R%.4f"% float(self.input_adj_angle.text())
|
||||
ACTION.CALL_MDI_WAIT(s, 30)
|
||||
|
||||
# selects probe routine group of buttons
|
||||
def input_next(self):
|
||||
next = self.stackedWidget_probe_type.currentIndex() +1
|
||||
if next == self.stackedWidget_probe_type.count():
|
||||
next = 0
|
||||
self.stackedWidget_probe_type.setCurrentIndex(next)
|
||||
|
||||
if self._runImmediately:
|
||||
return
|
||||
# Auto exclusive doesn't allow unchecking all buttons
|
||||
# We force it here
|
||||
if not self._cmd is None:
|
||||
button = self._cmd
|
||||
button.group().setExclusive(False)
|
||||
button.blockSignals(True)
|
||||
button.setChecked(False)
|
||||
button.blockSignals(False)
|
||||
button.group().setExclusive(True)
|
||||
self._cmd = None
|
||||
#####################################################
|
||||
# Entry callbacks
|
||||
#####################################################
|
||||
def update_search_vel_pin(self, text):
|
||||
try:
|
||||
self.pin_svel.set(float(text))
|
||||
except:
|
||||
pass
|
||||
def update_probe_vel_pin(self, text):
|
||||
try:
|
||||
self.pin_pvel.set(float(text))
|
||||
except:
|
||||
pass
|
||||
def update_probe_height_pin(self, text):
|
||||
try:
|
||||
self.pin_pheight.set(float(text))
|
||||
except:
|
||||
pass
|
||||
def update_block_height_pin(self, text):
|
||||
value = float(text)
|
||||
#if value == self.pin_bheight.get(): return
|
||||
origin = INFO.INI.getreal("AXIS_Z", "MIN_LIMIT") + value
|
||||
ACTION.CALL_MDI_WAIT( "G10 L2 P0 Z%s" % origin )
|
||||
try:
|
||||
self.pin_bheight.set(value)
|
||||
except:
|
||||
pass
|
||||
def update_latch_return_dist_pin(self, text):
|
||||
try:
|
||||
self.pin_latch_rtn.set(float(text))
|
||||
except:
|
||||
pass
|
||||
|
||||
#####################################################
|
||||
# Helper functions
|
||||
#####################################################
|
||||
|
||||
# return false if failed so other ways of reporting can be used.
|
||||
# there might not be a statusbar in main screen.
|
||||
def set_statusbar(self, msg, priority = DEFAULT, noLog = False):
|
||||
try:
|
||||
self.QTVCP_INSTANCE_.add_status(msg, priority, noLog)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_parms(self):
|
||||
self.send_dict = {key: self['input_' + key].text() for key in (self.parm_list)}
|
||||
for key in ['allow_auto_zero', 'allow_auto_skew']:
|
||||
val = '1' if self[key].isChecked() else '0'
|
||||
self.send_dict.update( {key: val} )
|
||||
# come from INI
|
||||
for key in ['ts_diam','z_max_clear','ts_x','ts_y','ts_z','ts_max','tool_diameter','tool_number']:
|
||||
val = str(self[key])
|
||||
if val == 'NONE': val = None
|
||||
self.send_dict.update( {key: val} )
|
||||
|
||||
def check_probe(self):
|
||||
try:
|
||||
self.led_probe_function_chk.setState(hal.get_value('motion.probe-input'))
|
||||
except:
|
||||
pass
|
||||
|
||||
def show_results(self, line):
|
||||
for key in self.status_list:
|
||||
if key in('th','bh'):
|
||||
if key == 'bh' and line[key] != 'None':
|
||||
self.input_tool_block_height.setText(line[key])
|
||||
elif line[key] != 'None':
|
||||
self.input_tool_probe_height.setText(line[key])
|
||||
elif line[key] != 'None':
|
||||
self['status_' + key].setText(line[key])
|
||||
else:
|
||||
self['status_' + key].setText('')
|
||||
|
||||
def pop_help(self):
|
||||
self.help.showDialog()
|
||||
|
||||
# run-immediately buttons are momentary
|
||||
# pre-select buttons are checkable
|
||||
# focus policy changes for 'cycle start button steering'
|
||||
# with pre-select option
|
||||
|
||||
def set_checkableButtons(self, state):
|
||||
if state:
|
||||
policy = Qt.ClickFocus
|
||||
else:
|
||||
policy = Qt.NoFocus
|
||||
for i in self.outside_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.inside_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.skew_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.length_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
for i in self.tool_buttonGroup.buttons():
|
||||
i.setCheckable(state)
|
||||
|
||||
########################################
|
||||
# required boiler code
|
||||
########################################
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
#########################################################################
|
||||
# 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 set_dialog_code(self, data):
|
||||
self.dialog_code = data
|
||||
def get_dialog_code(self):
|
||||
return self.dialog_code
|
||||
def reset_dialog_code(self):
|
||||
self.dialog_code = 'CALCULATOR'
|
||||
dialogCodeString = Property(str, get_dialog_code, set_dialog_code, reset_dialog_code)
|
||||
|
||||
def set_runImmediately(self, data):
|
||||
self._runImmediately = data
|
||||
self.set_checkableButtons(not data)
|
||||
def get_runImmediately(self):
|
||||
return self._runImmediately
|
||||
def reset_runImmediately(self):
|
||||
self._runImmediately = True
|
||||
|
||||
# toggle run on button push or run on function call
|
||||
runImmediately = Property(bool, get_runImmediately, set_runImmediately, reset_runImmediately)
|
||||
|
||||
class HelpDialog(QtWidgets.QDialog, GeometryMixin):
|
||||
def __init__(self, parent=None):
|
||||
super(HelpDialog, self).__init__(parent)
|
||||
self._title = 'Versa Help'
|
||||
self.setWindowFlags(self.windowFlags() | Qt.Tool |
|
||||
Qt.Dialog | Qt.WindowStaysOnTopHint |
|
||||
Qt.WindowSystemMenuHint)
|
||||
self.currentHelpPage=-1
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(600)
|
||||
self.helpPages = ['versa_usage.html','versa_usage1.html','versa_usage2.html',
|
||||
'versa_usage3.html','versa_usage4.html','versa_usage5.html',
|
||||
'versa_usage6.html','versa_usage7.html','versa_usage8.html']
|
||||
|
||||
def _hal_init(self):
|
||||
self.buildWidget()
|
||||
self.set_default_geometry()
|
||||
self.read_preference_geometry('VersaHelpDialog-geometry')
|
||||
|
||||
def buildWidget(self):
|
||||
|
||||
l = QtWidgets.QVBoxLayout()
|
||||
t = QtWidgets.QTextEdit('Versa Probe Help')
|
||||
t.setReadOnly(True)
|
||||
l.addWidget(t)
|
||||
|
||||
buttons = QDialogButtonBox()
|
||||
|
||||
closebutton = QtWidgets.QPushButton()
|
||||
closebutton.setIconSize(QtCore.QSize(38, 38))
|
||||
closebutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/standardbutton-cancel-128.png'))
|
||||
closebutton.clicked.connect(lambda : self.close())
|
||||
|
||||
nextbutton = QtWidgets.QPushButton()
|
||||
nextbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
nextbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/right-32.png'))
|
||||
nextbutton.clicked.connect(lambda : self.next(t,True))
|
||||
|
||||
previousbutton = QtWidgets.QPushButton()
|
||||
previousbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
previousbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/left-32.png'))
|
||||
previousbutton.clicked.connect(lambda : self.next(t,False))
|
||||
|
||||
self.pageStepUpbutton = QtWidgets.QPushButton()
|
||||
self.pageStepUpbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
self.pageStepUpbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/up-32.png'))
|
||||
self.pageStepUpbutton.clicked.connect(lambda : self.pageStep(t,False))
|
||||
|
||||
self.pageStepDwnbutton = QtWidgets.QPushButton()
|
||||
self.pageStepDwnbutton.setIconSize(QtCore.QSize(38, 38))
|
||||
self.pageStepDwnbutton.setIcon(QtGui.QIcon(':/qt-project.org/styles/commonstyle/images/down-32.png'))
|
||||
self.pageStepDwnbutton.clicked.connect(lambda : self.pageStep(t,True))
|
||||
|
||||
bBox = QDialogButtonBox(buttons)
|
||||
bBox.addButton(self.pageStepUpbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(self.pageStepDwnbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(previousbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(nextbutton, QDialogButtonBox.ActionRole)
|
||||
bBox.addButton(closebutton, QDialogButtonBox.DestructiveRole)
|
||||
bBox.rejected.connect(self.reject)
|
||||
|
||||
l.addWidget(bBox)
|
||||
self.setLayout(l)
|
||||
|
||||
# Load the first help page lazily (on first show): pre-scaling its
|
||||
# images at construction can hang or crash qtvcp headless.
|
||||
self._helpText = t
|
||||
self._helpLoaded = False
|
||||
|
||||
def _set_scaled_html(self, t, html):
|
||||
# QTextEdit scales raster images with a nearest-neighbour filter, which
|
||||
# makes the help diagrams look jagged. Pre-scale each raster <img> to
|
||||
# its requested size with a smooth filter and register it as a document
|
||||
# resource so Qt draws it 1:1. SVG images are left untouched so Qt's
|
||||
# vector renderer draws them crisply at any size.
|
||||
doc = t.document()
|
||||
for tag in re.findall(r'<img\b[^>]*>', html):
|
||||
match = re.search(r'src="([^"]+)"', tag)
|
||||
if match is None or match.group(1).lower().endswith('.svg'):
|
||||
continue
|
||||
src = match.group(1)
|
||||
image = QtGui.QImage(src)
|
||||
if image.isNull():
|
||||
continue
|
||||
width = re.search(r'width="(\d+)"', tag)
|
||||
height = re.search(r'height="(\d+)"', tag)
|
||||
if width is not None and int(width.group(1)) != image.width():
|
||||
image = image.scaledToWidth(int(width.group(1)), QtCore.Qt.SmoothTransformation)
|
||||
elif height is not None and int(height.group(1)) != image.height():
|
||||
image = image.scaledToHeight(int(height.group(1)), QtCore.Qt.SmoothTransformation)
|
||||
doc.addResource(QtGui.QTextDocument.ImageResource, QtCore.QUrl(src), image)
|
||||
t.setHtml(html)
|
||||
|
||||
def next(self,t,direction=None):
|
||||
if direction is None:
|
||||
self.currentHelpPage = 0
|
||||
elif direction:
|
||||
self.currentHelpPage +=1
|
||||
if self.currentHelpPage > len(self.helpPages)-1:
|
||||
self.currentHelpPage = len(self.helpPages)-1
|
||||
else:
|
||||
self.currentHelpPage -=1
|
||||
if self.currentHelpPage < 0:
|
||||
self.currentHelpPage = 0
|
||||
try:
|
||||
pagePath = os.path.join(HELP, self.helpPages[self.currentHelpPage])
|
||||
file = QtCore.QFile(pagePath)
|
||||
file.open(QtCore.QFile.ReadOnly)
|
||||
html = file.readAll()
|
||||
html = str(html, encoding='utf8')
|
||||
html = html.replace("../images/","{}/".format(PATH.IMAGEDIR))
|
||||
self._set_scaled_html(t, html)
|
||||
if t.verticalScrollBar().isVisible():
|
||||
t.verticalScrollBar().setPageStep(20)
|
||||
self.pageStepDwnbutton.show()
|
||||
self.pageStepUpbutton.show()
|
||||
else:
|
||||
self.pageStepDwnbutton.hide()
|
||||
self.pageStepUpbutton.hide()
|
||||
|
||||
except Exception as e:
|
||||
t.setText('Versa Probe Help file Unavailable:\n\n{}'.format(e))
|
||||
if direction is None:
|
||||
return
|
||||
self.show()
|
||||
|
||||
def pageStep(self, t, state):
|
||||
if state:
|
||||
t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepAdd)
|
||||
else:
|
||||
t.verticalScrollBar().triggerAction (QAbstractSlider.SliderPageStepSub)
|
||||
|
||||
# accept button applies presets and if line number given starts linuxcnc
|
||||
def close(self):
|
||||
self.record_geometry()
|
||||
super(HelpDialog, self).close()
|
||||
|
||||
def showDialog(self):
|
||||
if not self._helpLoaded:
|
||||
try:
|
||||
self.next(self._helpText)
|
||||
except Exception as e:
|
||||
self._helpText.setText('Versa Probe Help file Unavailable:\n\n{}'.format(e))
|
||||
self._helpLoaded = True
|
||||
self.setWindowTitle(self._title);
|
||||
self.set_geometry()
|
||||
retval = self.exec()
|
||||
LOG.debug('Value of pressed button: {}'.format(retval))
|
||||
|
||||
# look for a custom version of Versa Probe
|
||||
module = PATH.find_custom_widget_path('versa_probe.py','VersaProbeCustom')
|
||||
if not module:
|
||||
module = VersaProbeParent
|
||||
|
||||
class VersaProbe(module):
|
||||
def __init__(self, parent=None):
|
||||
super(VersaProbe, self).__init__(parent)
|
||||
|
||||
####################################
|
||||
# Testing
|
||||
####################################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = VersaProbeParent()
|
||||
w.setObjectName('versaprobeParent')
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
191
lib/python/qtvcp/widgets/virtualkeyboard.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
from qtpy import QtGui, QtWidgets, uic
|
||||
from qtpy.QtCore import Qt, QEvent, Signal
|
||||
|
||||
from qtvcp.core import Info, Path
|
||||
from qtvcp import logger
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
PATH = Path()
|
||||
|
||||
class VirtualKeyboard(QtWidgets.QWidget):
|
||||
hideKeyboard = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(VirtualKeyboard, self).__init__(parent)
|
||||
# Load the widgets UI file:
|
||||
self.filename = os.path.join(PATH.SHAREDIR,'widgets_ui', 'virtual_keyboard.ui')
|
||||
try:
|
||||
self.instance = uic.loadUi(self.filename, self)
|
||||
except AttributeError as e:
|
||||
LOG.critical(e)
|
||||
|
||||
self.NO_ORD_KEY_LIST = list()
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_Backspace)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_Enter)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_Escape)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_Delete)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F1)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F2)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F3)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F4)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F5)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F6)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F7)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F8)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F9)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F10)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F11)
|
||||
self.NO_ORD_KEY_LIST.append(Qt.Key_F12)
|
||||
|
||||
self.letter_list = "abcdefghijklmnopqrstuvwxyz"
|
||||
self.number_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'minus', 'equal']
|
||||
self.shift_number = [')', '!', '@', '#', '$', '%', '^', '&&', '*', '(']
|
||||
self.special_list = {
|
||||
'less': Qt.Key_Less,
|
||||
'greater': Qt.Key_Greater,
|
||||
'question': Qt.Key_Question,
|
||||
'colon': Qt.Key_Colon,
|
||||
'leftbrace': Qt.Key_BraceLeft,
|
||||
'rightbrace': Qt.Key_BraceRight,
|
||||
'doublequote': Qt.Key_QuoteDbl,
|
||||
'pipe': Qt.Key_Bar,
|
||||
'underscore': Qt.Key_Underscore,
|
||||
'plus': Qt.Key_Plus,
|
||||
'exclamation': Qt.Key_Exclam,
|
||||
'at': Qt.Key_At,
|
||||
'pound': Qt.Key_NumberSign,
|
||||
'dollar': Qt.Key_Dollar,
|
||||
'percent': Qt.Key_Percent,
|
||||
'carat': Qt.Key_AsciiCircum,
|
||||
'ampersand': Qt.Key_Ampersand,
|
||||
'asterisk': Qt.Key_Asterisk,
|
||||
'leftparenth': Qt.Key_ParenLeft,
|
||||
'rightparenth': Qt.Key_ParenRight,
|
||||
'space': Qt.Key_Space
|
||||
}
|
||||
|
||||
self.function_keys = {
|
||||
'0': Qt.Key_F10,
|
||||
'1': Qt.Key_F1,
|
||||
'2': Qt.Key_F2,
|
||||
'3': Qt.Key_F3,
|
||||
'4': Qt.Key_F4,
|
||||
'5': Qt.Key_F5,
|
||||
'6': Qt.Key_F6,
|
||||
'7': Qt.Key_F7,
|
||||
'8': Qt.Key_F8,
|
||||
'9': Qt.Key_F9,
|
||||
'minus': Qt.Key_F11,
|
||||
'equal': Qt.Key_F12
|
||||
}
|
||||
|
||||
self.init_letters()
|
||||
self.init_numbers()
|
||||
self.init_special()
|
||||
|
||||
# control button group
|
||||
self.btn_back.KEY_CHAR = Qt.Key_Backspace
|
||||
self.btn_delete.KEY_CHAR = Qt.Key_Delete
|
||||
self.btn_enter.KEY_CHAR = Qt.Key_Enter
|
||||
|
||||
self.letters_buttonGroup.buttonClicked.connect(self.letter_clicked)
|
||||
self.numbers_buttonGroup.buttonClicked.connect(self.special_clicked)
|
||||
self.special_buttonGroup.buttonClicked.connect(self.special_clicked)
|
||||
self.control_buttonGroup.buttonClicked.connect(self.button_clicked)
|
||||
self.btn_hide.clicked.connect(lambda: self.hideKeyboard.emit())
|
||||
|
||||
def init_letters(self):
|
||||
for val in self.letter_list:
|
||||
self['btn_' + val].setText(val)
|
||||
self['btn_' + val].KEY_CHAR = ord(val)
|
||||
|
||||
def init_numbers(self):
|
||||
for i, val in enumerate(self.number_list[:-2]):
|
||||
self['btn_' + val].setText(self.shift_number[i] + '\n' + val)
|
||||
self['btn_' + val].KEY_CHAR = ord(val)
|
||||
self.btn_minus.setText('_\n-')
|
||||
self.btn_minus.KEY_CHAR = Qt.Key_Minus
|
||||
self.btn_equal.setText('+\n=')
|
||||
self.btn_equal.KEY_CHAR = Qt.Key_Equal
|
||||
|
||||
def init_special(self):
|
||||
self.btn_comma.KEY_CHAR = Qt.Key_Comma
|
||||
self.btn_period.KEY_CHAR = Qt.Key_Period
|
||||
self.btn_semicolon.KEY_CHAR = Qt.Key_Semicolon
|
||||
self.btn_slash.KEY_CHAR = Qt.Key_Slash
|
||||
self.btn_backslash.KEY_CHAR = Qt.Key_Backslash
|
||||
self.btn_apostrophe.KEY_CHAR = Qt.Key_Apostrophe
|
||||
self.btn_left_bracket.KEY_CHAR = Qt.Key_BracketLeft
|
||||
self.btn_right_bracket.KEY_CHAR = Qt.Key_BracketRight
|
||||
self.btn_space.KEY_CHAR = Qt.Key_Space
|
||||
|
||||
def letter_clicked(self, btn):
|
||||
if self.btn_shift.isChecked():
|
||||
btn.KEY_CHAR = ord(btn.text().upper())
|
||||
self.button_clicked(btn)
|
||||
btn.KEY_CHAR = ord(btn.text())
|
||||
|
||||
def special_clicked(self, btn):
|
||||
key_save = btn.KEY_CHAR
|
||||
if self.btn_shift.isChecked() and not self.btn_function.isChecked():
|
||||
key = btn.property('upper')
|
||||
btn.KEY_CHAR = self.special_list.get(key)
|
||||
self.button_clicked(btn)
|
||||
btn.KEY_CHAR = key_save
|
||||
|
||||
def button_clicked(self, btn):
|
||||
fw = QtWidgets.QApplication.focusWidget()
|
||||
if fw is None: return
|
||||
if btn.KEY_CHAR in self.NO_ORD_KEY_LIST:
|
||||
keyPress = QtGui.QKeyEvent(QEvent.KeyPress, btn.KEY_CHAR, Qt.NoModifier, '')
|
||||
else:
|
||||
if self.btn_shift.isChecked():
|
||||
modifier = Qt.ShiftModifier
|
||||
self.btn_shift.setChecked(False)
|
||||
else:
|
||||
modifier = Qt.NoModifier
|
||||
keyPress = QtGui.QKeyEvent(QEvent.KeyPress, btn.KEY_CHAR, modifier, chr(btn.KEY_CHAR))
|
||||
try:
|
||||
QtWidgets.QApplication.sendEvent(fw, keyPress)
|
||||
except:
|
||||
pass
|
||||
|
||||
def caps_lock_clicked(self, state):
|
||||
if state:
|
||||
for val in self.letter_list:
|
||||
self['btn_' + val].setText(val.upper())
|
||||
self['btn_' + val].KEY_CHAR = ord(val.upper())
|
||||
else:
|
||||
self.init_letters()
|
||||
|
||||
def function_clicked(self, state):
|
||||
if state:
|
||||
for val in self.number_list:
|
||||
alt_txt = self['btn_' + val].property('fcode')
|
||||
self['btn_' + val].setText(alt_txt)
|
||||
for key, val in list(self.function_keys.items()):
|
||||
self['btn_' + key].KEY_CHAR = val
|
||||
else:
|
||||
self.init_numbers()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
#############################
|
||||
# Testing #
|
||||
#############################
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import *
|
||||
from qtpy.QtCore import *
|
||||
from qtpy.QtGui import *
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
w = VirtualKeyboard()
|
||||
w.show()
|
||||
sys.exit( app.exec() )
|
||||
|
||||
47
lib/python/qtvcp/widgets/web_widget.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtCore import QUrl, QFile, QUrl
|
||||
from qtpy.QtWidgets import (QWidget,QVBoxLayout,QLabel)
|
||||
from qtvcp.core import Path
|
||||
|
||||
# Set up logging
|
||||
from qtvcp import logger
|
||||
LOG = logger.getLogger(__name__)
|
||||
PATH = Path()
|
||||
|
||||
good = True
|
||||
try:
|
||||
from qtpy.QtWebEngineWidgets import QWebEngineView as WebBase
|
||||
except:
|
||||
try:
|
||||
from qtpy.QtWebKitWidgets import QWebView as WebBase
|
||||
except:
|
||||
LOG.warning('WebWidget - Is python3-pyqt-QtWebEngine installed?')
|
||||
# fail safe - mostly for designer
|
||||
# PyQt5.QtWebEngineWidgets must be loaded before QApplication
|
||||
# which doesn't happen in designer. Also screen won't immediately
|
||||
# crash if both libraries are not missing
|
||||
WebBase = QWidget
|
||||
good = False
|
||||
|
||||
class WebWidget(WebBase):
|
||||
def __init__(self, parent=None):
|
||||
super(WebWidget, self).__init__(parent)
|
||||
|
||||
# bad imports - give a clue
|
||||
if not good:
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addStretch(1)
|
||||
mess = QLabel('WebWidget Import failed')
|
||||
vbox.addWidget(mess)
|
||||
|
||||
# load a HTML file, but try to fix the image path
|
||||
def loadFile(self, path):
|
||||
file = QFile(path)
|
||||
file.open(QFile.ReadOnly)
|
||||
txt = file.readAll()
|
||||
txt = str(txt, encoding='utf8')
|
||||
# fix sample html image path
|
||||
if 'IMAGEDIR/' in txt:
|
||||
txt = txt.replace('IMAGEDIR/','')
|
||||
super().setHtml(txt,QUrl.fromLocalFile(str(PATH.IMAGEDIR+'/')))
|
||||
182
lib/python/qtvcp/widgets/widget_baseclass.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
# qtvcp baseclass
|
||||
#
|
||||
# Copyright (c) 2017 Chris Morley <chrisinnanaimo@hotmail.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
|
||||
# (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.
|
||||
#
|
||||
# _HalWidgetBase is the base class for most pyQt widgets
|
||||
# the other subclasses are for simple HAL widget functionality
|
||||
|
||||
import hal
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtWidgets import QDialog
|
||||
|
||||
from qtvcp import logger
|
||||
|
||||
# Instantiate the libraries with global reference
|
||||
# LOG is for running code logging
|
||||
LOG = logger.getLogger(__name__)
|
||||
|
||||
# Force the log level for this module
|
||||
#LOG.setLevel(logger.WARNING) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
###########################
|
||||
""" Set of base classes """
|
||||
###########################
|
||||
|
||||
|
||||
class _HalWidgetBase_(object):
|
||||
def __init__(self,comp=None,path=None,window=None):
|
||||
super(_HalWidgetBase_, self).__init__()
|
||||
# only initialize once for all instances
|
||||
if self.__class__._instanceNum >=1:
|
||||
return
|
||||
# embed these variables in all instances
|
||||
self.__class__._pin_name_ = ''
|
||||
self.__class__.HAL_GCOMP_ = comp
|
||||
self.__class__.PATHS_ = path
|
||||
self.__class__.QTVCP_INSTANCE_ = window
|
||||
if not window is None:
|
||||
self.__class__.SETTINGS_ = window.settings
|
||||
self.__class__._instanceNum += 1
|
||||
#print self.__class__._instanceNum >=1
|
||||
#print 'comp',comp,self.__class__._instanceNum
|
||||
|
||||
# INSTANCE_NAME is the embedded panel name
|
||||
def hal_init(self, HAL_NAME=None,INSTANCE_NAME = None):
|
||||
self.__class__.QTVCP_INSTANCE_.registerHalWidget(self)
|
||||
|
||||
if INSTANCE_NAME is not None:
|
||||
self.__class__.THIS_INSTANCE_ = self.__class__.QTVCP_INSTANCE_[INSTANCE_NAME]
|
||||
else:
|
||||
self.__class__.THIS_INSTANCE_ = self.__class__.QTVCP_INSTANCE_
|
||||
if HAL_NAME is not None:
|
||||
self.HAL_NAME_ = str(HAL_NAME)
|
||||
else:
|
||||
if self.objectName() =='':
|
||||
LOG.warning('No objectName or HAL_NAME specified for object: {}'.format(self))
|
||||
self.HAL_NAME_ = self.objectName()
|
||||
self.QT_OBJECT_ = self
|
||||
try:
|
||||
self.PREFS_ = self.QTVCP_INSTANCE_.PREFS_
|
||||
except:
|
||||
self.PREFS_ = None
|
||||
|
||||
# register avaliable dialogs (for external controls)
|
||||
if isinstance(self, QDialog):
|
||||
idname = self.objectName()
|
||||
LOG.verbose('green<Registered Dialog:> {}'.format(idname))
|
||||
self.__class__.QTVCP_INSTANCE_.registerDialog(self)
|
||||
|
||||
LOG.verbose("HAL_init: ObjectName:'{}'\n SELF:{}\n HAL NAME:{}\n PREFS:{}\n INSTANMCE:{}".format(self.objectName(),self,self.HAL_NAME_,self.PREFS_ ,self.__class__.THIS_INSTANCE_))
|
||||
self._hal_init()
|
||||
|
||||
def _hal_init(self):
|
||||
""" Child HAL initialization functions """
|
||||
pass
|
||||
|
||||
def _hal_cleanup(self):
|
||||
""" Child HAL closing functions """
|
||||
pass
|
||||
|
||||
def _designer_init(self):
|
||||
""" Child Designer editor plugin initialization functions """
|
||||
pass
|
||||
|
||||
def get_full_pinname(self, pin):
|
||||
""" Returns the component and pin name as a combined string """
|
||||
n = self.HAL_GCOMP_.comp.getprefix()
|
||||
p = pin.get_name()
|
||||
return n+'.'+p
|
||||
|
||||
# we do this so we can manipulate all instances based on this.
|
||||
# we wish to embed variables.
|
||||
# This class gets get instantiated in qt_makegui.py
|
||||
class _HalWidgetBase(_HalWidgetBase_):
|
||||
_instance = None
|
||||
_instanceNum = 0
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
cls._instance = _HalWidgetBase_.__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
def __setitem__(self, item, value):
|
||||
return setattr(self, item, value)
|
||||
|
||||
class _HalToggleBase(_HalWidgetBase):
|
||||
def _hal_init(self):
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_BIT, hal.HAL_OUT)
|
||||
self.hal_pin_not = self.HAL_GCOMP_.newpin(pname + "-not", hal.HAL_BIT, hal.HAL_OUT)
|
||||
self.toggled.connect(lambda data: self._pin_update(data))
|
||||
|
||||
def _pin_update(self, state):
|
||||
self.hal_pin.set(bool(state))
|
||||
self.hal_pin_not.set(not bool(state))
|
||||
|
||||
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 = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
|
||||
class _HalScaleBase(_HalWidgetBase):
|
||||
def _hal_init(self):
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
self.hal_pin_f = self.HAL_GCOMP_.newpin(pname + "-f", hal.HAL_FLOAT, hal.HAL_OUT)
|
||||
self.hal_pin_s = self.HAL_GCOMP_.newpin(pname + "-s", hal.HAL_S32, hal.HAL_OUT)
|
||||
self.valueChanged.connect(lambda data: self._pin_update(data))
|
||||
# default scale
|
||||
self.input = 1
|
||||
# force pin update
|
||||
self.valueChanged.emit(self.value())
|
||||
|
||||
def _pin_update(self, data):
|
||||
self.hal_pin_f.set(data * self.input)
|
||||
self.hal_pin_s.set(int(data * self.input))
|
||||
|
||||
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 = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
|
||||
# reacts to HAL pin changes
|
||||
class _HalSensitiveBase(_HalWidgetBase):
|
||||
def _hal_init(self):
|
||||
if self._pin_name_ == '':
|
||||
pname = self.HAL_NAME_
|
||||
else:
|
||||
pname = self._pin_name_
|
||||
self.hal_pin = self.HAL_GCOMP_.newpin(pname, hal.HAL_BIT, hal.HAL_IN)
|
||||
self.hal_pin.value_changed.connect(lambda s: self.setEnabled(s))
|
||||
|
||||
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 = Property(str, get_pin_name, set_pin_name, reset_pin_name)
|
||||
|
||||
BIN
lib/python/qtvcp/widgets/widget_icons/actionbutton.png
Normal file
|
After Width: | Height: | Size: 408 B |
BIN
lib/python/qtvcp/widgets/widget_icons/axistoolbutton.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/basicprobe.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/calculator.png
Normal file
|
After Width: | Height: | Size: 883 B |
BIN
lib/python/qtvcp/widgets/widget_icons/checkbox.png
Normal file
|
After Width: | Height: | Size: 817 B |
BIN
lib/python/qtvcp/widgets/widget_icons/dial.png
Normal file
|
After Width: | Height: | Size: 978 B |
BIN
lib/python/qtvcp/widgets/widget_icons/dialogbuttonbox.png
Normal file
|
After Width: | Height: | Size: 1003 B |
BIN
lib/python/qtvcp/widgets/widget_icons/doublescale.png
Normal file
|
After Width: | Height: | Size: 749 B |
BIN
lib/python/qtvcp/widgets/widget_icons/dro_label.png
Normal file
|
After Width: | Height: | Size: 238 B |
BIN
lib/python/qtvcp/widgets/widget_icons/filemanager.png
Normal file
|
After Width: | Height: | Size: 549 B |
BIN
lib/python/qtvcp/widgets/widget_icons/focusoverlay.png
Normal file
|
After Width: | Height: | Size: 503 B |
BIN
lib/python/qtvcp/widgets/widget_icons/gauge.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/gcodeeditor.png
Normal file
|
After Width: | Height: | Size: 563 B |
BIN
lib/python/qtvcp/widgets/widget_icons/gcodegraphics.png
Normal file
|
After Width: | Height: | Size: 405 B |
BIN
lib/python/qtvcp/widgets/widget_icons/generalhalinput.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
lib/python/qtvcp/widgets/widget_icons/generalhaloutput.png
Normal file
|
After Width: | Height: | Size: 564 B |
BIN
lib/python/qtvcp/widgets/widget_icons/hallabel.png
Normal file
|
After Width: | Height: | Size: 953 B |
BIN
lib/python/qtvcp/widgets/widget_icons/jogincrements.png
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
lib/python/qtvcp/widgets/widget_icons/lcdnumber.png
Normal file
|
After Width: | Height: | Size: 555 B |
BIN
lib/python/qtvcp/widgets/widget_icons/lcnc_led.png
Normal file
|
After Width: | Height: | Size: 847 B |
BIN
lib/python/qtvcp/widgets/widget_icons/led.png
Normal file
|
After Width: | Height: | Size: 847 B |
BIN
lib/python/qtvcp/widgets/widget_icons/linuxcnc-wizard.gif
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/mdihistory.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/mdiline.png
Normal file
|
After Width: | Height: | Size: 405 B |
BIN
lib/python/qtvcp/widgets/widget_icons/originoffsetview.png
Normal file
|
After Width: | Height: | Size: 135 B |
BIN
lib/python/qtvcp/widgets/widget_icons/progress.png
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
lib/python/qtvcp/widgets/widget_icons/pushbutton.png
Normal file
|
After Width: | Height: | Size: 408 B |
BIN
lib/python/qtvcp/widgets/widget_icons/radioaxisselector.png
Normal file
|
After Width: | Height: | Size: 549 B |
BIN
lib/python/qtvcp/widgets/widget_icons/radiobutton.png
Normal file
|
After Width: | Height: | Size: 586 B |
BIN
lib/python/qtvcp/widgets/widget_icons/screenoptions.png
Normal file
|
After Width: | Height: | Size: 1016 B |
BIN
lib/python/qtvcp/widgets/widget_icons/slider.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
lib/python/qtvcp/widgets/widget_icons/statusadjustmentbar.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
lib/python/qtvcp/widgets/widget_icons/statuslabel.png
Normal file
|
After Width: | Height: | Size: 953 B |