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

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