797 lines
33 KiB
Python
797 lines
33 KiB
Python
#!/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())
|