Import LinuxCNC source tree

This commit is contained in:
Wang Dequan
2026-08-20 06:17:39 +08:00
commit 2c814634c3
9429 changed files with 10556013 additions and 0 deletions

View 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.

File diff suppressed because it is too large Load Diff

View 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()

View 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()

View 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()

View 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)

View 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()

View 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()

View 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() )

View 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())

View 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())

View 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)

View 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_( ))

View 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

File diff suppressed because it is too large Load Diff

View 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()

View 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())

View 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

View 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())

File diff suppressed because it is too large Load Diff

View 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())

View 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() )

View 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)

View 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)

View 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)

View 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()

View 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()

File diff suppressed because it is too large Load Diff

View 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())

View 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() )

View 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())

View 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()

View 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())

View 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() )

View 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()

View 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() )

View 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())

View 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()

View 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()

View 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())

View 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()

File diff suppressed because it is too large Load Diff

View 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()

View 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

View 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()

View 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()

View 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() )

View 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())

View 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() )

File diff suppressed because it is too large Load Diff

View 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)

View 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())

View 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())

View 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())

View 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)

View 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))

View 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())

View 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()

View 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))

View 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() )

View 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())

View 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() )

View 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()

View 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() )

View 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() )

View 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+'/')))

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Some files were not shown because too many files have changed in this diff Show More