VERSION = '016.084'
LCNCVER = '2.10'

'''
qtplasmac_handler.py

Copyright (C) 2020-2025 Phillip A Carter
Copyright (C) 2020-2026 Gregory D Carl

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
51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
'''

import os
import sys
from shutil import copy as COPY
from subprocess import Popen, PIPE
from subprocess import run as RUN
from subprocess import call as CALL
from importlib import reload, util
import time
import tarfile
import math
import glob
import linuxcnc
import hal
from OpenGL.GL import glTranslatef
from qtpy import QtCore, QtWidgets, QtGui
from qtpy.QtCore import *
from qtpy.QtWidgets import *
from qtpy.QtGui import *
try:
    from qtpy.Qsci import QsciScintilla
except ImportError:
    try:
        from PyQt5.Qsci import QsciScintilla
    except ImportError:
        from PyQt6.Qsci import QsciScintilla
from qtvcp import logger
from qtvcp.core import Status, Action, Info, Tool
try:
    from qtpy.QtWebEngineWidgets import QWebEngineView as _QWebEngineView
    _WEBENGINE_AVAILABLE = True
except Exception:
    _WEBENGINE_AVAILABLE = False
from qtvcp.lib.gcodes import GCodes
from qtvcp.lib.keybindings import Keylookup
from qtvcp.lib.preferences import Access
from qtvcp.lib.qtplasmac import tooltips as TOOLTIPS
from qtvcp.lib.qtplasmac import set_offsets as OFFSETS
from qtvcp.lib.qtplasmac import updater as UPDATER
from qtvcp.lib.qtplasmac.interlock_rules import INTERLOCK_RULES
from qtvcp.widgets.camview_widget import CamView as CAM
from qtvcp.widgets.file_manager import FileManager as FILE_MAN
from qtvcp.widgets.gcode_editor import GcodeEditor as EDITOR
from qtvcp.widgets.gcode_editor import GcodeDisplay as DISPLAY
from qtvcp.widgets.mdi_history import MDIHistory as MDI_HISTORY
from qtvcp.widgets.mdi_line import MDILine as MDI_LINE
from qtvcp.widgets.origin_offsetview import OriginOffsetView as OFFSETVIEW
from qtvcp.widgets.origin_offsetview import MyTableModel as OFFSET_TABLE
from qtvcp.widgets.screen_options import ScreenOptions as OPTIONS
from qtvcp.widgets.simple_widgets import DoubleScale as DOUBLESCALE
from qtvcp.widgets.status_label import StatusLabel as STATLABEL
from qtvcp.widgets.stylesheeteditor import StyleSheetEditor as SSE
from qtvcp.lib.aux_program_loader import Aux_program_loader
from plasmac import run_from_line as RFL
from rs274.glcanon import GlCanonDraw as DRAW
from qt5_graphics import Lcnc_3dGraphics as DRO

LOG = logger.getLogger(__name__)
KEYBIND = Keylookup()
STATUS = Status()
INFO = Info()
ACTION = Action()
TOOL = Tool()
AUX_PRGM = Aux_program_loader()
INIPATH = os.environ.get('INI_FILE_NAME', '/dev/null')

_translate = QCoreApplication.translate


# a vertical line as a separator on the status bar
class VLine(QFrame):
    def __init__(self):
        super(VLine, self).__init__()
        self.setFrameShape(self.VLine | self.Plain)


# dummy class to raise an exception for style issues
class ColorError(Exception):
    pass


# click signal for some labels
def click_signal(widget):
    class Filter(QObject):
        clicked = Signal()

        def eventFilter(self, obj, event):
            if obj == widget:
                if event.type() == QEvent.MouseButtonRelease:
                    if obj.rect().contains(event.pos()):
                        self.clicked.emit()
                        return True
            return False
    filter = Filter(widget)
    widget.installEventFilter(filter)
    return filter.clicked


# the main handler
class HandlerClass:
    from qtvcp.lib.qtplasmac import conversational as CONV

    # when self.w.button_frame changes size
    def eventFilter(self, object, event):
        if event.type() == QtCore.QEvent.Resize:
            self.size_changed(object)
        return True

    def __init__(self, halcomp, widgets, paths):
        self.interlockRules = INTERLOCK_RULES.copy()
        self.state = {
            'all_homed': False,
            'consumable_change': False,
            'consumable_changing': False,
            'estop_cleared': False,
            'file_bounds_error': False,
            'file_opened': False,
            'framing': False,
            'gcode_loaded': False,
            'gcodestack_is_mdi': False,
            'interp_idle': True,
            'interp_paused': False,
            'interp_running': False,
            'laser_button_state': True,
            'machine_on': False,
            'manual_cut_active': False,
            'offsets_active': False,
            'ohmic_probe_enable': False,
            'ohmic_test': False,
            'plasmac_idle': True,
            'previewstack_is_edit': False,
            'previewstack_is_camera': False,
            'previewstack_is_user_manual': False,
            'previewstack_is_offsets': False,
            'previewstack_is_open': False,
            'previewstack_is_preview': True,
            'probe_bounds_error': False,
            'probe_test': False,
            'rfl_dialog': False,
            'single_cut_dialog': False,
            'torch_enable': False,
            'torch_pulse': False,
        }
        self.firstRun = True
        self.h = halcomp
        self.w = widgets
        self.h.comp.setprefix('qtplasmac')
        self.PATHS = paths
        self.iniFile = INFO.INI
        self.foreColor = '#ffee06'
        # can we find M190 in the USER_M_PATH
        # if not we will attempt to find a valid USER_M_PATH path later
        self.mPath = self.iniFile.find('RS274NGC', 'USER_M_PATH').split(':')
        for path in self.mPath:
            if path.startswith('.'):
                path = os.path.join(self.PATHS.CONFIGPATH, path)
            if os.path.isfile(os.path.join(path, 'M190')):
                self.mPath = 'valid'
                break
        self.machineName = self.iniFile.find('EMC', 'MACHINE')
        self.machineTitle = f'{self.machineName} - QtPlasmaC v{LCNCVER}-{VERSION}, powered by QtVCP and LinuxCNC'
        self.docsVer = 'devel' if 'pre' in linuxcnc.version else LCNCVER
        self.prefsFile = os.path.join(self.PATHS.CONFIGPATH, self.machineName + '.prefs')
        self.materialFile = os.path.join(self.PATHS.CONFIGPATH, self.machineName + '_material.cfg')
        self.unitsPerMm = 1
        self.units = self.iniFile.find('TRAJ', 'LINEAR_UNITS')
        if self.units == 'inch':
            self.units = 'in'
            self.unitsPerMm = 0.03937
        # open prefs file so we can use it for updates
        # it will not exist prior to V1.222.170 2022/03/08
        if os.path.isfile(self.prefsFile):
            self.PREFS = Access(self.prefsFile)
        else:
            self.PREFS = None
        self.updateIni = {}
        self.updateData = []
        self.update_check()
        self.PREFS = Access(self.prefsFile)
        self.MATS = Access(self.materialFile)
        self.STYLEEDITOR = SSE(widgets, paths)
        self.GCODES = GCodes(widgets)
        self.IMAGES = os.path.join(self.PATHS.IMAGEDIR, 'qtplasmac/images/')
        self.landscape = True
        if os.path.basename(self.PATHS.XML) == 'qtplasmac_9x16.ui':
            self.landscape = False
        self.upFile = os.path.join(self.PATHS.CONFIGPATH, 'user_periodic.py')
        self.umUrl = QUrl(f'https://linuxcnc.org/docs/{self.docsVer}/html/plasma/qtplasmac.html')
        KEYBIND.add_call('Key_F12', 'on_keycall_F12')
        KEYBIND.add_call('Key_F9', 'on_keycall_F9')
        KEYBIND.add_call('Key_Plus', 'on_keycall_PLUS')
        KEYBIND.add_call('Key_Minus', 'on_keycall_MINUS')
        KEYBIND.add_call('Key_R', 'on_keycall_RUN')
        KEYBIND.add_call('Key_Any', 'on_keycall_PAUSE')
        KEYBIND.add_call('Key_o', 'on_keycall_OPEN')
        KEYBIND.add_call('Key_l', 'on_keycall_LOAD')
        KEYBIND.add_call('Key_j', 'on_keycall_JOINT')
        KEYBIND.add_call('Key_1', 'on_keycall_NUMBER', 1)
        KEYBIND.add_call('Key_Exclam', 'on_keycall_NUMBER', 1)
        KEYBIND.add_call('Key_2', 'on_keycall_NUMBER', 2)
        KEYBIND.add_call('Key_At', 'on_keycall_NUMBER', 2)
        KEYBIND.add_call('Key_3', 'on_keycall_NUMBER', 3)
        KEYBIND.add_call('Key_NumberSign', 'on_keycall_NUMBER', 3)
        KEYBIND.add_call('Key_4', 'on_keycall_NUMBER', 4)
        KEYBIND.add_call('Key_Dollar', 'on_keycall_NUMBER', 4)
        KEYBIND.add_call('Key_5', 'on_keycall_NUMBER', 5)
        KEYBIND.add_call('Key_Percent', 'on_keycall_NUMBER', 5)
        KEYBIND.add_call('Key_6', 'on_keycall_NUMBER', 6)
        KEYBIND.add_call('Key_AsciiCircum', 'on_keycall_NUMBER', 6)
        KEYBIND.add_call('Key_7', 'on_keycall_NUMBER', 7)
        KEYBIND.add_call('Key_Ampersand', 'on_keycall_NUMBER', 7)
        KEYBIND.add_call('Key_8', 'on_keycall_NUMBER', 8)
        KEYBIND.add_call('Key_Asterisk', 'on_keycall_NUMBER', 8)
        KEYBIND.add_call('Key_9', 'on_keycall_NUMBER', 9)
        KEYBIND.add_call('Key_ParenLeft', 'on_keycall_NUMBER', 9)
        KEYBIND.add_call('Key_0', 'on_keycall_NUMBER', 0)
        KEYBIND.add_call('Key_ParenRight', 'on_keycall_NUMBER', 0)
        # there are no default keys for joint/axis 4
        KEYBIND.add_call('Key_Comma', 'on_keycall_BPOS')
        KEYBIND.add_call('Key_Less', 'on_keycall_BPOS')
        KEYBIND.add_call('Key_Period', 'on_keycall_BNEG')
        KEYBIND.add_call('Key_Greater', 'on_keycall_BNEG')
        KEYBIND.add_call('Key_End', 'on_keycall_END')
        KEYBIND.add_call('Key_Delete', 'on_keycall_DELETE')
        KEYBIND.add_call('Alt+Key_Return', 'on_keycall_ALTRETURN')
        KEYBIND.add_call('Alt+Key_Enter', 'on_keycall_ALTRETURN')
        KEYBIND.add_call('Key_Return', 'on_keycall_RETURN')
        KEYBIND.add_call('Key_Enter', 'on_keycall_RETURN')
        KEYBIND.add_call('Key_QuoteLeft', 'on_keycall_QUOTELEFT')
        KEYBIND.add_call('Key_AsciiTilde', 'on_keycall_QUOTELEFT')
        self.axes = {'valid': [x.lower() for x in INFO.AVAILABLE_AXES]}
        for axis in 'abc':
            self.axes[axis] = []
            for n in ['dro_0', 'dro_label_0', 'home_0', 'touch_0', 'jog_0_plus', 'jog_0_minus']:
                self.axes[axis].append(n.replace('0', axis))
        self.systemList = ['G53', 'G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3']
        self.slowJogFactor = 10
        self.jogFast = False
        self.jogSlow = False
        self.lastLoadedProgram = 'None'
        self.set_interlock_defaults()
        self.jogButtonList = ['jog_x_plus', 'jog_x_minus', 'jog_y_plus', 'jog_y_minus',
                              'jog_z_plus', 'jog_z_minus', 'jog_a_plus', 'jog_a_minus',
                              'jog_b_plus', 'jog_b_minus', 'jog_c_plus', 'jog_c_minus']
        self.jogSyncList = []
        self.xMin = float(self.iniFile.find('AXIS_X', 'MIN_LIMIT'))
        self.xMax = float(self.iniFile.find('AXIS_X', 'MAX_LIMIT'))
        self.yMin = float(self.iniFile.find('AXIS_Y', 'MIN_LIMIT'))
        self.yMax = float(self.iniFile.find('AXIS_Y', 'MAX_LIMIT'))
        self.zMin = float(self.iniFile.find('AXIS_Z', 'MIN_LIMIT'))
        self.zMax = float(self.iniFile.find('AXIS_Z', 'MAX_LIMIT'))
        self.xLen = self.xMax - self.xMin
        self.yLen = self.yMax - self.yMin
        self.thcFeedRate = float(self.iniFile.find('AXIS_Z', 'MAX_VELOCITY')) * \
            float(self.iniFile.find('AXIS_Z', 'OFFSET_AV_RATIO')) * 60
        self.offsetFeedRate = min(float(self.iniFile.find('AXIS_X', 'MAX_VELOCITY')) * 30,
                                  float(self.iniFile.find('AXIS_Y', 'MAX_VELOCITY')) * 30,
                                  float(self.iniFile.find('TRAJ', 'MAX_LINEAR_VELOCITYs') or 100000))
        self.maxHeight = self.zMax - self.zMin
        self.maxPidP = self.thcFeedRate / self.unitsPerMm * 0.1
        self.tmpPath = '/tmp/qtplasmac/'
        if not os.path.isdir(self.tmpPath):
            os.mkdir(self.tmpPath)
        self.tmpMaterialGcode = f'{self.tmpPath}{self.machineName}_material.gcode'
        self.gcodeErrorFile = f'{self.tmpPath}gcode_errors.txt'
        self.materialUpdate = False
        self.autoChange = False
        self.pmx485Exists = False
        self.pmx485Connected = False
        self.pmx485CommsError = False
        self.pmx485FaultCode = 0.0
        self.pmx485ArcTime = 0.0
        self.pmx485LabelState = None
        self.currentX = self.currentY = 0
        self.degreeSymbol = u"\u00b0"
        self.cameraOn = False
        self.fTmp = f'{self.tmpPath}temp.ngc'
        self.fNgc = f'{self.tmpPath}shape.ngc'
        self.fNgcBkp = f'{self.tmpPath}backup.ngc'
        self.fNgcSent = f'{self.tmpPath}sent_shape.ngc'
        self.filteredBkp = f'{self.tmpPath}filtered_bkp.ngc'
        self.oldConvButton = False
        self.convWidgetsLoaded = False
        self.programPrefix = self.iniFile.find('DISPLAY', 'PROGRAM_PREFIX') or os.environ['LINUXCNC_NCFILES_DIR']
        self.dialogError = False
        self.cutTypeText = ''
        self.heightOvr = 0.0
        self.heightOvrScale = 0.1
        self.old_ovr_counts = 0
        self.startLine = 0
        self.preRflFile = ''
        self.preClearFile = ''
        self.rflActive = False
        self.torchOn = False
        self.progRun = False
        self.rapidOn = False
        self.probeOn = False
        self.gcodeProps = {}
        self.framing = False
        self.fileBoundsError = False
        self.probeBoundsError = False
        self.singleCutDialog = False
        self.ohmicTest = False
        self.obLayout = ''
        self.notifyColor = 'normal'
        self.firstHoming = False
        self.droScale = 1
        self.mdiError = False
        self.extLaserButton = False
        self.virtualMachine = False
        self.realTimeDelay = False
        self.preSingleCutMaterial = None
        self.preFileSaveMaterial = None
        self.laserDryRunCoords = None
        self.runType = {'type': 'end'}
        # plasmac states
        self.IDLE           =  0
        self.PROBE_HEIGHT   =  1
        self.PROBE_DOWN     =  2
        self.PROBE_UP       =  3
        self.ZERO_HEIGHT    =  4
        self.PIERCE_HEIGHT  =  5
        self.TORCH_ON       =  6
        self.ARC_OK         =  7
        self.PIERCE_DELAY   =  8
        self.PUDDLE_JUMP    =  9
        self.CUT_HEIGHT     = 10
        self.CUT_MODE_01    = 11
        self.CUT_MODE_2     = 12
        self.PAUSE_AT_END   = 13
        self.SAFE_HEIGHT    = 14
        self.MAX_HEIGHT     = 15
        self.END_CUT        = 16
        self.END_JOB        = 17
        self.TORCH_PULSE    = 18
        self.PAUSED_MOTION  = 19
        self.OHMIC_TEST     = 20
        self.PROBE_TEST     = 21
        self.SCRIBING       = 22
        self.CONS_CHNG_ON   = 23
        self.CONS_CHNG_OFF  = 24
        self.CUT_REC_ON     = 25
        self.CUT_REC_OFF    = 26
        self.DEBUG          = 27
        # main tab widget tab indexes
        self.MAIN = 0
        self.CONVERSATIONAL = 1
        self.PARAMETERS = 2
        self.SETTINGS = 3
        self.STATISTICS = 4
        # map index to name for interlock rules
        self.tabNames = {
            self.MAIN: 'main_tab',
            self.CONVERSATIONAL: 'conversational_tab',
            self.PARAMETERS: 'parameters_tab',
            self.SETTINGS: 'settings_tab',
            self.STATISTICS: 'statistics_tab',
        }
        # preview stack indexes
        self.PREVIEW = 0
        self.OPEN = 1
        self.EDIT = 2
        self.CAMERA = 3
        self.OFFSETS = 4
        self.USER_MANUAL = 5
        # gcode stack indexes
        self.GCODE = 0
        self.MDI = 1
        # jog stack indexes
        self.JOG = 0
        self.CUT_RECOVERY = 1
        # tool indexes
        self.TORCH = 0
        self.SCRIBE = 1
        self.UNKNOWN = 2
        self.tool = self.TORCH

# called by qtvcp.py
    def initialized__(self):
        head = _translate('HandlerClass', 'QtPlasmaC GUI Message')
        if '.'.join(linuxcnc.version.split('.')[:2]) != LCNCVER:
            msg0 = _translate('HandlerClass', 'LinuxCNC version should be')
            msg1 = _translate('HandlerClass', 'The detected version is')
            msg2 = _translate('HandlerClass', 'QtPlasmac is closing')
            msg = f'{head}:\n{msg0} {LCNCVER}\n\n{msg1} {linuxcnc.version.split(".")[:2]}\n\n{msg2}\n'
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, msg)
            quit()
        # if USER_M_PATH is not valid try to find a valid USER_M_PATH in the possible default locations
        if self.mPath != 'valid' and not self.updateIni:
            msg0 = _translate('HandlerClass', 'cannot be found in the path')
            msg1 = _translate('HandlerClass', 'Please edit [RS274NGC]USER_M_PATH in the .ini file')
            msg2 = _translate('HandlerClass', 'QtPlasmac is closing')
            msg3 = _translate('HandlerClass', 'does exist in')
            for path in ['/usr/share/doc/linuxcnc/examples/nc_files/plasmac/m_files', self.PATHS.BASEDIR, self.PATHS.CONFIGPATH]:
                mPath = self.find_a_file('M190', path)
                if mPath:
                    break
            if not mPath:
                msg3 = _translate('HandlerClass', 'does not exist in the default locations')
            msg = f'{head}:\nM190 {msg0}:\n{":".join(self.mPath)}\n\n{msg1}\n\nM190 {msg3}:\n{mPath[:-5]}\n\n{msg2}\n'
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, msg)
            quit()
        ucFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac_custom.py')
        if os.path.isfile(ucFile):
            exec(compile(open(ucFile, 'rb').read(), ucFile, 'exec'))
        # ensure we get all startup errors
        STATUS.connect('error', self.error_update)
        STATUS.connect('graphics-gcode-error', lambda o, e: self.error_update(o, linuxcnc.OPERATOR_ERROR, e))
        STATUS.emit('update-machine-log', f'--- {self.machineTitle} ---', None)
        self.make_hal_pins()
        self.init_preferences()
        self.hide_widgets()
        self.init_widgets()
        # hijack the qtvcp shutdown to our own close event
        self.w.screen_options.QTVCP_INSTANCE_.closeEvent = self.closeEvent
        self.w.button_frame.installEventFilter(self.w)
        self.link_hal_pins()
        self.statistics_init()
        self.set_axes_and_joints()
        self.set_spinbox_parameters()
        self.load_plasma_parameters()
        self.set_mode()
        self.user_button_setup()
        self.load_material_file()
        self.offset_peripherals()
        self.set_probe_offset_pins()
        STATUS.connect('state-estop', lambda w: self.estop_state(True))
        STATUS.connect('state-estop-reset', lambda w: self.estop_state(False))
        STATUS.connect('state-on', lambda w: self.power_state(True))
        STATUS.connect('state-off', lambda w: self.power_state(False))
        STATUS.connect('hard-limits-tripped', self.hard_limit_tripped)
        STATUS.connect('user-system-changed', self.user_system_changed)
        STATUS.connect('file-loaded', self.file_loaded)
        STATUS.connect('homed', self.joint_homed)
        STATUS.connect('all-homed', self.joints_all_homed)
        STATUS.connect('not-all-homed', self.joint_unhomed)
        STATUS.connect('gcode-line-selected', lambda w, line: self.set_start_line(line))
        STATUS.connect('graphics-line-selected', lambda w, line: self.set_start_line(line))
        STATUS.connect('g-code-changed', self.gcodes_changed)
        STATUS.connect('m-code-changed', self.mcodes_changed)
        STATUS.connect('program-pause-changed', self.pause_changed)
        STATUS.connect('graphics-loading-progress', self.progress_changed)
        STATUS.connect('interp-paused', self.interp_paused)
        STATUS.connect('interp-idle', self.interp_idle)
        STATUS.connect('interp-reading', self.interp_reading)
        STATUS.connect('interp-waiting', self.interp_waiting)
        STATUS.connect('interp-run', self.interp_running)
        STATUS.connect('jograte-changed', self.jog_rate_changed)
        STATUS.connect('graphics-gcode-properties', lambda w, d: self.update_gcode_properties(d))
        STATUS.connect('system_notify_button_pressed', self.system_notify_button_pressed)
        STATUS.connect('tool-in-spindle-changed', self.tool_changed)
        STATUS.connect('periodic', lambda w: self.update_periodic())
        STATUS.connect('metric-mode-changed', self.metric_mode_changed)
        STATUS.connect('motion-type-changed', lambda w, data: self.motion_type_changed(data))
        self.startupTimer = QTimer()
        self.startupTimer.timeout.connect(self.startup_timeout)
        self.startupTimer.setSingleShot(True)
        self.shutdownTimer = QTimer()
        self.shutdownTimer.timeout.connect(self.shutdown_timeout)
        self.shutdownTime = 2000
        self.laserTimer = QTimer()
        self.laserTimer.timeout.connect(self.laser_timeout)
        self.laserTimer.setSingleShot(True)
        self.ohmicLedTimer = QTimer()
        self.ohmicLedTimer.timeout.connect(self.ohmic_led_timeout)
        self.ohmicLedTimer.setSingleShot(True)
        self.watchDogTimer = QTimer()
        self.watchDogTimer.timeout.connect(self.watchdog_has_bit)
        self.watchDogTimer.setSingleShot(True)
        self.watchDogState = None
        self.set_color_styles()
        self.vm_check()
        # set hal pins only after initialized__ has begun
        # some locales won't set pins before this phase
        self.thcFeedRatePin.set(self.thcFeedRate)
        self.pmPort = None
        if self.PREFS.getpref('Port', '', str, 'POWERMAX'):
            self.pmPort = self.PREFS.getpref('Port', '', str, 'POWERMAX')
        if self.pmPort and self.pmx485_check(self.pmPort):
            self.pmx485_startup(self.pmPort)
        else:
            self.w.gas_pressure.hide()
            self.w.gas_pressure_label.hide()
            self.w.cut_mode.hide()
            self.w.cut_mode_label.hide()
            self.w.pmx485_frame.hide()
            self.w.pmx_stats_frame.hide()
        if self.w.mdihistory.rows:
            self.mdiLast = self.w.mdihistory.model.item(self.w.mdihistory.rows - 1).text()
        else:
            self.mdiLast = None
        self.w.mdihistory.MDILine.spindle_inhibit(True)
        self.w.mdihistory.MDILine.g92_inhibit(True)
        if self.updateIni:
            self.update_iniwrite()
        updateLog = os.path.join(self.PATHS.CONFIGPATH, 'update_log.txt')
        if self.updateData:
            restart = False
            msgType = linuxcnc.OPERATOR_TEXT
            msgText = ''
            with open(updateLog, 'a') as f:
                for update in self.updateData:
                    if update[2]:
                        f.write(f'{time.strftime("%y-%m-%d")} {update[2]}\n')
                        msgText += f'{update[2]}\n'
                        if update[0]:
                            restart = True
                        if update[1]:
                            msgType = linuxcnc.OPERATOR_ERROR
            head = _translate('HandlerClass', 'QtPlasmaC Configuration Update')
            STATUS.emit('error', msgType, f'{head}:\n{msgText}\n')
            if restart:
                msg = _translate('HandlerClass', 'Due to configuration changes a restart is required')
                STATUS.emit('error', linuxcnc.OPERATOR_TEXT, f'{head}:\n{msg}\n')
                quit()
        if not os.path.isfile(updateLog):
            with open(updateLog, 'w') as f:
                f.write(f'{time.strftime("%y-%m-%d")} Initial    V{LCNCVER}-{VERSION}\n')
        self.startupTimer.start(250)
        # turning off autorepeat should always be the last thing intialized__ does
        self.autorepeat_keys(False)

# called by qtvcp.py, can override qtvcp settings or qtvcp allowed user options (via INI)
    def before_loop__(self):
        self.w.setWindowTitle(self.machineTitle)
        self.iconPath = 'share/icons/hicolor/scalable/apps/linuxcnc_alt/linuxcncicon_plasma.svg'
        appPath = os.path.realpath(os.path.dirname(sys.argv[0]))
        self.iconBase = '/usr' if appPath == '/usr/bin' else appPath.replace('/bin', '/debian/extras/usr')
        self.w.setWindowIcon(QIcon(os.path.join(self.iconBase, self.iconPath)))

#########################################################################################################################
# CLASS PATCHING SECTION #
# note that the widget must be initialized before it can be patched (Example: gcodegraphics needs patched later)
#########################################################################################################################

    # called by qtvcp.py
    def class_patch__(self):
        self.file_manager_patch()
        self.gcode_editor_patch()
        self.camview_patch()
        self.offset_table_patch()
        self.qt5_graphics_patch()
        self.screen_options_patch()

# patched file manager functions
    def file_manager_patch(self):
        self.old_load = FILE_MAN.load
        FILE_MAN.load = self.new_load

    # remove temporary materials before loading a file
    def new_load(self, fname=None):
        try:
            if fname is None:
                self.w.filemanager._getPathActivated()
                return
            self.w.filemanager.recordBookKeeping()
            self.remove_temp_materials()
            ACTION.OPEN_PROGRAM(fname)
            STATUS.emit('update-machine-log', 'Loaded: ' + fname, 'TIME')
        except Exception as e:
            LOG.error(f'Load file error: {e}')
            STATUS.emit('error', linuxcnc.NML_ERROR, f'Load file error: {e}')

# patched gcode editor functions
    def gcode_editor_patch(self):
        self.old_saveReturn = EDITOR.saveReturn
        EDITOR.saveReturn = self.new_saveReturn
        self.old_openReturn = EDITOR.openReturn
        EDITOR.openReturn = self.new_openReturn
        self.old_exitCall = EDITOR.exitCall
        EDITOR.exitCall = self.new_exitCall
        self.old_gcodeLexerCall = EDITOR.gcodeLexerCall
        EDITOR.gcodeLexerCall = self.new_gcodeLexerCall
        self.old_pythonLexerCall = EDITOR.pythonLexerCall
        EDITOR.pythonLexerCall = self.new_pythonLexerCall
        self.old_returnFromDialog = EDITOR.returnFromDialog
        EDITOR.returnFromDialog = self.new_returnFromDialog
        DISPLAY.load_preference = self.new_load_preference
        self.old_set_line_number = DISPLAY.set_line_number
        DISPLAY.set_line_number = self.new_set_line_number

    # save a non gcode file and don't load it into linuxcnc
    def new_saveReturn(self, filename):
        saved = ACTION.SAVE_PROGRAM(self.w.gcode_editor.editor.text(), filename)
        if saved is not None:
            self.w.gcode_editor.editor.setModified(False)
            if saved[-3:] in ['ngc', '.nc', 'tap']:
                self.preFileSaveMaterial = int(self.w.materials_box.currentText().split(': ', 1)[0])
                if self.rflActive:
                    self.clear_rfl()
                self.remove_temp_materials()
                ACTION.OPEN_PROGRAM(saved)

    # open a non gcode file and don't load it into linuxcnc
    def new_openReturn(self, filename):
        if filename[-3:] in ['ngc', '.nc', 'tap']:
            self.remove_temp_materials()
            ACTION.OPEN_PROGRAM(filename)
        else:
            self.w.gcode_editor.editor.load_text(filename)
        self.w.gcode_editor.editor.setModified(False)

    # modify the closing of the gcode editor
    def new_exitCall(self, index):
        proceed = self.editor_close_check()
        if not proceed:
            return
        self.w.preview_stack.setCurrentIndex(self.PREVIEW)
        if self.fileOpened and self.w.gcode_editor.editor.isModified():
            self.w.gcode_editor.editor.setModified(False)
            self.file_reload_clicked()
        elif not self.fileOpened:
            self.w.gcode_editor.editor.new_text()
            self.w.gcode_editor.editor.setModified(False)
            self.view_t_pressed(self.w.gcodegraphics)
        self.w.gcode_editor.editMode()
        self.vkb_hide()
        ACTION.SET_MANUAL_MODE()

    # we don't use lexer colors
    def new_gcodeLexerCall(self):
        pass

    # we don't use lexer colors
    def new_pythonLexerCall(self):
        pass

    # don't allow rfl.ngc as a file name
    def new_returnFromDialog(self, w, message):
        if message.get('NAME') == self.w.gcode_editor.load_dialog_code:
            path = message.get('RETURN')
            code = bool(message.get('ID') == '%s__' % self.w.gcode_editor.objectName())
            if path and code:
                self.w.gcode_editor.openReturn(path)
        elif message.get('NAME') == self.w.gcode_editor.save_dialog_code:
            path = message.get('RETURN')
            code = bool(message.get('ID') == '%s__' % self.w.gcode_editor.objectName())
            if path and code:
                if not os.path.basename(path) in ['rfl', 'rfl.ngc']:
                    self.w.gcode_editor.saveReturn(path)
                else:
                    head = _translate('HandlerClass', 'Save Error')
                    msg0 = _translate('HandlerClass', 'The file name')
                    msg1 = _translate('HandlerClass', 'is not allowed')
                    self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0} "{os.path.basename(path)}" {msg1}\n\n')
                    self.w.gcode_editor.getSaveFileName()
                    return

    # load the qtplasmac preferences file rather than the qtvcp preferences file
    def new_load_preference(self, w):
        self.w.gcode_editor.editor.load_text(self.prefsFile)
        self.w.gcode_editor.editor.setCursorPosition(self.w.gcode_editor.editor.lines(), 0)

    # dont highlight lines selected from the editor in the preview window
    def new_set_line_number(self, line):
        if self.w.sender():
            if self.w.sender().objectName() == 'gcode_editor_display':
                return
            else:
                STATUS.emit('gcode-line-selected', line+1)

# patched camera functions
    def camview_patch(self):
        self.old_wheelEvent = CAM.wheelEvent
        CAM.wheelEvent = self.new_wheelEvent
        self.old_drawText = CAM.drawText
        CAM.drawText = self.new_drawText
        self.old_mousePressEvent = CAM.mousePressEvent
        CAM.mousePressEvent = self.new_mousePressEvent
        self.old_mouseDoubleClickEvent = CAM.mouseDoubleClickEvent
        CAM.mouseDoubleClickEvent = self.new_mouseDoubleClickEvent

    # format the angle display
    def new_drawText(self, event, qp):
        qp.setPen(self.w.camview.text_color)
        qp.setFont(self.w.camview.font)
        if self.w.camview.pix:
            qp.drawText(self.w.camview.rect(), QtCore.Qt.AlignTop, f'{self.w.camview.rotation:0.3f}{self.degreeSymbol}')
        else:
            qp.drawText(self.w.camview.rect(), QtCore.Qt.AlignCenter, self.w.camview.text)

    # limit scale and diameter, don't allow mouse rotation
    def new_wheelEvent(self, event):
        mouseState = qApp.mouseButtons()
        w = self.w.camview.size().width()
        h = self.w.camview.size().height()
        if event.angleDelta().y() < 0:
            if mouseState == QtCore.Qt.NoButton:
                self.w.camview.diameter -= 2
            if mouseState == QtCore.Qt.LeftButton:
                self.w.camview.scale -= .1
        else:
            if mouseState == QtCore.Qt.NoButton:
                self.w.camview.diameter += 2
            if mouseState == QtCore.Qt.LeftButton:
                self.w.camview.scale += .1
        if self.w.camview.diameter < 2:
            self.w.camview.diameter = 2
        if self.w.camview.diameter > w - 5:
            self.w.camview.diameter = w - 5
        if self.w.camview.diameter > h - 5:
            self.w.camview.diameter = h - 5
        if self.w.camview.scale < 1:
            self.w.camview.scale = 1
        if self.w.camview.scale > 5:
            self.w.camview.scale = 5

    # inhibit mouse single clicks
    def new_mousePressEvent(self, event):
        pass

    # don't reset rotation with double click
    def new_mouseDoubleClickEvent(self, event):
        if event.button() & QtCore.Qt.LeftButton:
            self.w.camview.scale = 1
        elif event.button() & QtCore.Qt.MiddleButton:
            self.w.camview.diameter = 20

# patched offset table functions
    def offset_table_patch(self):
        self.old_flags = OFFSET_TABLE.flags
        OFFSET_TABLE.flags = self.new_flags

    # we don't allow editing z axis or g92 offsets
    def new_flags(self, index):
        if not index.isValid():
            return None
        if index.column() == 9 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
        # prevent z axis offset editing
        elif index.column() == 2:
            return Qt.ItemIsEnabled
        # prevent g92 offset editing
        elif index.row() == 2:
            return Qt.ItemIsEnabled
        else:
            return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable

# patched qt5_graphics functions
    def qt5_graphics_patch(self):
        self.old_dro_format = DRO.dro_format
        DRO.dro_format = self.new_dro_format

    # replace dro with current material
    def new_dro_format(self, s, spd, dtg, limit, homed, positions, axisdtg, g5x_offset, g92_offset, tlo_offset):
        text = self.get_overlay_text()
        return limit, homed, text, text

# patched screen options functions
    def screen_options_patch(self):
        self.old_process_error = OPTIONS.process_error
        OPTIONS.process_error = self.new_process_error

    # we want custom notifications for jog errors
    def new_process_error(self, w, kind, text):
        self.realTimeDelay = False
        O = self.w.screen_options
        N = O.QTVCP_INSTANCE_._NOTICE
        if 'jog-inhibit' in text:
            if self.w.led_float_switch.hal_pin.get():
                text = _translate('HandlerClass', 'Float Switch has disabled jogging')
            elif self.ohmicLedInPin.get():
                text = _translate('HandlerClass', 'Ohmic Probe has disabled jogging')
            elif self.w.led_breakaway_switch.hal_pin.get():
                text = _translate('HandlerClass', 'Breakaway Switch has disabled jogging')
        elif 'jog-stop' in text or 'jog-stop-immediate' in text:
            if self.w.led_float_switch.hal_pin.get():
                text = _translate('HandlerClass', 'Float Switch has aborted active jogging')
            elif self.ohmicLedInPin.get():
                text = _translate('HandlerClass', 'Ohmic Probe has aborted active jogging')
            elif self.w.led_breakaway_switch.hal_pin.get():
                text = _translate('HandlerClass', 'Breakaway Switch has aborted active jogging')
        elif self.virtualMachine and 'unexpected realtime delay' in text.lower():
            text = f'Error suppressed:\n"{text.strip()}"\nRealtime delays are expected in a virtual environment'
            self.realTimeDelay = True
        if O.desktop_notify:
            if 'on limit switch error' in text:
                N.update(O.notify_hard_limits, title='Machine Error:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.OPERATOR_ERROR and not self.realTimeDelay:
                N.update(O.notify_critical, icon='dialog-error', title='Operator Error:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.OPERATOR_TEXT:
                N.update(O.notify_critical, icon='dialog-information', title='Operator Text:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.OPERATOR_DISPLAY:
                N.update(O.notify_critical, icon='dialog-messages', title='Operator Display:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.NML_ERROR:
                N.update(O.notify_critical, title='Internal NML Error:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.NML_TEXT:
                N.update(O.notify_critical, title='Internal NML Text:', message=text, msgs=O.notify_max_msgs)
            elif kind == linuxcnc.NML_DISPLAY:
                N.update(O.notify_critical, title='Internal NML Display:', message=text, msgs=O.notify_max_msgs)
            elif kind == STATUS.TEMPORARY_MESSAGE:
                N.update(O.notify_normal,
                         title='Operator Info:',
                         message=text,
                         status_timeout=0,
                         timeout=2,
                         msgs=O.notify_max_msgs)
        if O.play_sounds and O.mchnMsg_play_sound:
            STATUS.emit('play-sound', '%s' % O.mchnMsg_sound_type)
            if O.mchnMsg_speak_errors:
                if kind in (linuxcnc.OPERATOR_ERROR, linuxcnc.NML_ERROR):
                    STATUS.emit('play-sound', 'SPEAK %s ' % text)
            if O.mchnMsg_speak_text:
                if kind in (linuxcnc.OPERATOR_TEXT, linuxcnc.NML_TEXT,
                            linuxcnc.OPERATOR_DISPLAY, STATUS.TEMPORARY_MESSAGE):
                    STATUS.emit('play-sound', 'SPEAK %s ' % text)
        STATUS.emit('update-machine-log', text, 'TIME')


#########################################################################################################################
# SPECIAL FUNCTIONS SECTION #
#########################################################################################################################

    def make_hal_pins(self):
        self.consChangePin = self.h.newpin('consumable_changing', hal.HAL_BIT, hal.HAL_IN)
        self.convTabDisable = self.h.newpin('conv_disable', hal.HAL_BIT, hal.HAL_IN)
        self.cutTypePin = self.h.newpin('cut_type', hal.HAL_S32, hal.HAL_IN)
        self.developmentPin = self.h.newpin('development', hal.HAL_BIT, hal.HAL_IN)
        self.extAbortPin = self.h.newpin('ext_abort', hal.HAL_BIT, hal.HAL_IN)
        self.extAutoVoltsEnablePin = self.h.newpin('ext_auto_volts_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extChangeConsPin = self.h.newpin('ext_consumables', hal.HAL_BIT, hal.HAL_IN)
        self.extCornerLockEnablePin = self.h.newpin('ext_cornerlock_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecCancelPin = self.h.newpin('ext_cutrec_cancel', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecRevPin = self.h.newpin('ext_cutrec_rev', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecFwdPin = self.h.newpin('ext_cutrec_fwd', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecNPin = self.h.newpin('ext_cutrec_n', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecNEPin = self.h.newpin('ext_cutrec_ne', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecEPin = self.h.newpin('ext_cutrec_e', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecSEPin = self.h.newpin('ext_cutrec_se', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecSPin = self.h.newpin('ext_cutrec_s', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecSWPin = self.h.newpin('ext_cutrec_sw', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecWPin = self.h.newpin('ext_cutrec_w', hal.HAL_BIT, hal.HAL_IN)
        self.extCutRecNWPin = self.h.newpin('ext_cutrec_nw', hal.HAL_BIT, hal.HAL_IN)
        self.extFramingPin = self.h.newpin('ext_frame_job', hal.HAL_BIT, hal.HAL_IN)
        self.extHeightOvrCountsPin = self.h.newpin('ext_height_ovr_counts', hal.HAL_S32, hal.HAL_IN)
        self.extHeightOvrCountEnablePin = self.h.newpin('ext_height_ovr_count_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extHeightOvrMinusPin = self.h.newpin('ext_height_ovr_minus', hal.HAL_BIT, hal.HAL_IN)
        self.extHeightOvrPlusPin = self.h.newpin('ext_height_ovr_plus', hal.HAL_BIT, hal.HAL_IN)
        self.extHeightOvrResetPin = self.h.newpin('ext_height_ovr_reset', hal.HAL_BIT, hal.HAL_IN)
        self.extHeightOvrScalePin = self.h.newpin('ext_height_ovr_scale', hal.HAL_FLOAT, hal.HAL_IN)
        self.extIgnoreArcOkPin = self.h.newpin('ext_ignore_arc_ok', hal.HAL_BIT, hal.HAL_IN)
        self.extJogSlowPin = self.h.newpin('ext_jog_slow', hal.HAL_BIT, hal.HAL_IN)
#        self.extKerfCrossEnablePin = self.h.newpin('ext_kerfcross_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extLaserTouchOffPin = self.h.newpin('ext_laser_touchoff', hal.HAL_BIT, hal.HAL_IN)
        self.extLaserTogglePin = self.h.newpin('ext_laser_toggle', hal.HAL_BIT, hal.HAL_IN)
        self.extMeshModePin = self.h.newpin('ext_mesh_mode', hal.HAL_BIT, hal.HAL_IN)
        self.extOhmicPin = self.h.newpin('ext_ohmic', hal.HAL_BIT, hal.HAL_IN)
        self.extOhmicProbeEnablePin = self.h.newpin('ext_ohmic_probe_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extPausePin = self.h.newpin('ext_pause', hal.HAL_BIT, hal.HAL_IN)
        self.extPauseOnlyPin = self.h.newpin('ext_pause_only', hal.HAL_BIT, hal.HAL_IN)
        self.extPowerPin = self.h.newpin('ext_power', hal.HAL_BIT, hal.HAL_IN)
        self.extProbePin = self.h.newpin('ext_probe', hal.HAL_BIT, hal.HAL_IN)
        self.extPulsePin = self.h.newpin('ext_pulse', hal.HAL_BIT, hal.HAL_IN)
        self.extResumePin = self.h.newpin('ext_resume', hal.HAL_BIT, hal.HAL_IN)
        self.extRunPausePin = self.h.newpin('ext_run_pause', hal.HAL_BIT, hal.HAL_IN)
        self.extRunPin = self.h.newpin('ext_run', hal.HAL_BIT, hal.HAL_IN)
        self.extThcEnablePin = self.h.newpin('ext_thc_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extTorchEnablePin = self.h.newpin('ext_torch_enable', hal.HAL_BIT, hal.HAL_IN)
        self.extTouchOffPin = self.h.newpin('ext_touchoff', hal.HAL_BIT, hal.HAL_IN)
        self.extVoidLockEnablePin = self.h.newpin('ext_voidlock_enable', hal.HAL_BIT, hal.HAL_IN)
        self.gcodeScalePin = self.h.newpin('gcode_scale', hal.HAL_FLOAT, hal.HAL_OUT)
        self.heightOverridePin = self.h.newpin('height_override', hal.HAL_FLOAT, hal.HAL_OUT)
        self.jogInhibited = self.h.newpin('jog_inhibited', hal.HAL_BIT, hal.HAL_IN)
        self.laserOnPin = self.h.newpin('laser_on', hal.HAL_BIT, hal.HAL_OUT)
        self.offsetSetProbePin = self.h.newpin('offset_set_probe', hal.HAL_BIT, hal.HAL_OUT)
        self.offsetSetScribePin = self.h.newpin('offset_set_scribe', hal.HAL_BIT, hal.HAL_OUT)
        self.laserRecStatePin = self.h.newpin('laser_recovery_state', hal.HAL_S32, hal.HAL_IN)
        self.materialChangePin = self.h.newpin('material_change', hal.HAL_S32, hal.HAL_IN)
        self.materialChangeNumberPin = self.h.newpin('material_change_number', hal.HAL_S32, hal.HAL_IN)
        self.materialChangeTimeoutPin = self.h.newpin('material_change_timeout', hal.HAL_BIT, hal.HAL_IN)
        self.materialReloadPin = self.h.newpin('material_reload', hal.HAL_BIT, hal.HAL_IN)
        self.materialTempPin = self.h.newpin('material_temp', hal.HAL_S32, hal.HAL_IN)
        self.offsetsActivePin = self.h.newpin('offsets_active', hal.HAL_BIT, hal.HAL_IN)
        self.ohmicLedInPin = self.h.newpin('ohmic_led_in', hal.HAL_BIT, hal.HAL_IN)
        self.paramTabDisable = self.h.newpin('param_disable', hal.HAL_BIT, hal.HAL_IN)
        self.settingsTabDisable = self.h.newpin('settings_disable', hal.HAL_BIT, hal.HAL_IN)
        self.simStyleUpdate = self.h.newpin('sim_style_update', hal.HAL_BIT, hal.HAL_OUT)
        self.plasmacStatePin = self.h.newpin('plasmac_state', hal.HAL_S32, hal.HAL_IN)
        self.plasmacStopPin = self.h.newpin('plasmac_stop', hal.HAL_S32, hal.HAL_IN)
        self.pmx485CurrentPin = self.h.newpin('pmx485_current', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485CurrentMaxPin = self.h.newpin('pmx485_current_max', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485CurrentMinPin = self.h.newpin('pmx485_current_min', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485FaultPin = self.h.newpin('pmx485_fault', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485ModePin = self.h.newpin('pmx485_mode', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485PressurePin = self.h.newpin('pmx485_pressure', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485PressureMaxPin = self.h.newpin('pmx485_pressure_max', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485PressureMinPin = self.h.newpin('pmx485_pressure_min', hal.HAL_FLOAT, hal.HAL_IN)
        self.pmx485StatusPin = self.h.newpin('pmx485_status', hal.HAL_BIT, hal.HAL_IN)
        self.pmx485ArcTimePin = self.h.newpin('pmx485_arc_time', hal.HAL_FLOAT, hal.HAL_IN)
        self.probeTestErrorPin = self.h.newpin('probe_test_error', hal.HAL_BIT, hal.HAL_IN)
        self.out0Pin = self.h.newpin('ext_out_0', hal.HAL_BIT, hal.HAL_OUT)
        self.out1Pin = self.h.newpin('ext_out_1', hal.HAL_BIT, hal.HAL_OUT)
        self.out2Pin = self.h.newpin('ext_out_2', hal.HAL_BIT, hal.HAL_OUT)
        self.sensorActive = self.h.newpin('sensor_active', hal.HAL_BIT, hal.HAL_IN)
        self.tabsAlwaysEnabled = self.h.newpin('tabs_always_enabled', hal.HAL_BIT, hal.HAL_IN)
        self.thcFeedRatePin = self.h.newpin('thc_feed_rate', hal.HAL_FLOAT, hal.HAL_OUT)
        self.velReduct = self.h.newpin('vel_reduct', hal.HAL_FLOAT, hal.HAL_IN)
        self.xOffsetPin = self.h.newpin('x_offset', hal.HAL_FLOAT, hal.HAL_IN)
        self.yOffsetPin = self.h.newpin('y_offset', hal.HAL_FLOAT, hal.HAL_IN)
        self.zHeightPin = self.h.newpin('z_height', hal.HAL_FLOAT, hal.HAL_IN)
        self.zOffsetPin = self.h.newpin('z_offset_counts', hal.HAL_S32, hal.HAL_IN)
        self.xMinPierceExtentPin = self.h.newpin('x_min_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN)
        self.xMaxPierceExtentPin = self.h.newpin('x_max_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN)
        self.yMinPierceExtentPin = self.h.newpin('y_min_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN)
        self.yMaxPierceExtentPin = self.h.newpin('y_max_pierce_extent', hal.HAL_FLOAT, hal.HAL_IN)

    def link_hal_pins(self):
        # arc parameters
        CALL(['halcmd', 'net', 'plasmac:arc-fail-delay', 'qtplasmac.arc_fail_delay-f', 'plasmac.arc-fail-delay'])
        CALL(['halcmd', 'net', 'plasmac:arc-max-starts', 'qtplasmac.arc_max_starts-s', 'plasmac.arc-max-starts'])
        CALL(['halcmd', 'net', 'plasmac:restart-delay', 'qtplasmac.arc_restart_delay-f', 'plasmac.restart-delay'])
        CALL(['halcmd', 'net', 'plasmac:arc-voltage-scale', 'qtplasmac.arc_voltage_scale-f', 'plasmac.arc-voltage-scale'])
        CALL(['halcmd', 'net', 'plasmac:arc-voltage-offset', 'qtplasmac.arc_voltage_offset-f', 'plasmac.arc-voltage-offset'])
        CALL(['halcmd', 'net', 'plasmac:height-per-volt', 'qtplasmac.height_per_volt-f', 'plasmac.height-per-volt'])
        CALL(['halcmd', 'net', 'plasmac:arc-ok-high', 'qtplasmac.arc_ok_high-f', 'plasmac.arc-ok-high'])
        CALL(['halcmd', 'net', 'plasmac:arc-ok-low', 'qtplasmac.arc_ok_low-f', 'plasmac.arc-ok-low'])
        # thc parameters
        CALL(['halcmd', 'net', 'plasmac:thc-feed-rate', 'qtplasmac.thc_feed_rate', 'plasmac.thc-feed-rate'])
        CALL(['halcmd', 'net', 'plasmac:thc-auto', 'qtplasmac.thc_auto', 'plasmac.thc-auto'])
        CALL(['halcmd', 'net', 'plasmac:thc-delay', 'qtplasmac.thc_delay-f', 'plasmac.thc-delay'])
        CALL(['halcmd', 'net', 'plasmac:thc-sample-counts', 'qtplasmac.thc_sample_counts-s', 'plasmac.thc-sample-counts'])
        CALL(['halcmd', 'net', 'plasmac:thc-sample-threshold', 'qtplasmac.thc_sample_threshold-f', 'plasmac.thc-sample-threshold'])
        CALL(['halcmd', 'net', 'plasmac:thc-threshold', 'qtplasmac.thc_threshold-f', 'plasmac.thc-threshold'])
        CALL(['halcmd', 'net', 'plasmac:pid-p-gain', 'qtplasmac.pid_p_gain-f', 'plasmac.pid-p-gain'])
        CALL(['halcmd', 'net', 'plasmac:pid-i-gain', 'qtplasmac.pid_i_gain-f', 'plasmac.pid-i-gain'])
        CALL(['halcmd', 'net', 'plasmac:pid-d-gain', 'qtplasmac.pid_d_gain-f', 'plasmac.pid-d-gain'])
        CALL(['halcmd', 'net', 'plasmac:cornerlock-threshold', 'qtplasmac.cornerlock_threshold-f', 'plasmac.cornerlock-threshold'])
        CALL(['halcmd', 'net', 'plasmac:voidlock-slope', 'qtplasmac.voidlock_slope-s', 'plasmac.voidlock-slope'])
        # probe parameters
        CALL(['halcmd', 'net', 'plasmac:float-switch-travel', 'qtplasmac.float_switch_travel-f', 'plasmac.float-switch-travel'])
        CALL(['halcmd', 'net', 'plasmac:probe-feed-rate', 'qtplasmac.probe_feed_rate-f', 'plasmac.probe-feed-rate'])
        CALL(['halcmd', 'net', 'plasmac:probe-start-height', 'qtplasmac.probe_start_height-f', 'plasmac.probe-start-height'])
        CALL(['halcmd', 'net', 'plasmac:ohmic-probe-offset', 'qtplasmac.ohmic_probe_offset-f', 'plasmac.ohmic-probe-offset'])
        CALL(['halcmd', 'net', 'plasmac:ohmic-max-attempts', 'qtplasmac.ohmic_max_attempts-s', 'plasmac.ohmic-max-attempts'])
        CALL(['halcmd', 'net', 'plasmac:skip-ihs-distance', 'qtplasmac.skip_ihs_distance-f', 'plasmac.skip-ihs-distance'])
        CALL(['halcmd', 'net', 'plasmac:slat-height', 'qtplasmac.slat_height-f', 'plasmac.slat-height'])
        CALL(['halcmd', 'net', 'plasmac:offset-feed-rate', 'qtplasmac.offset_feed_rate-f', 'plasmac.offset-feed-rate'])
        # safety parameters
        CALL(['halcmd', 'net', 'plasmac:safe-height', 'qtplasmac.safe_height-f', 'plasmac.safe-height'])
        # scribe parameters
        CALL(['halcmd', 'net', 'plasmac:scribe-arm-delay', 'qtplasmac.scribe_arm_delay-f', 'plasmac.scribe-arm-delay'])
        CALL(['halcmd', 'net', 'plasmac:scribe-on-delay', 'qtplasmac.scribe_on_delay-f', 'plasmac.scribe-on-delay'])
        # spotting parameters
        CALL(['halcmd', 'net', 'plasmac:spotting-threshold', 'qtplasmac.spotting_threshold-f', 'plasmac.spotting-threshold'])
        CALL(['halcmd', 'net', 'plasmac:spotting-time', 'qtplasmac.spotting_time-f', 'plasmac.spotting-time'])
        # motion parameters
        CALL(['halcmd', 'net', 'plasmac:setup-feed-rate', 'qtplasmac.setup_feed_rate-f', 'plasmac.setup-feed-rate'])
        # material parameters
        CALL(['halcmd', 'net', 'plasmac:cut-feed-rate', 'qtplasmac.cut_feed_rate-f', 'plasmac.cut-feed-rate'])
        CALL(['halcmd', 'net', 'plasmac:cut-height', 'qtplasmac.cut_height-f', 'plasmac.cut-height'])
        CALL(['halcmd', 'net', 'plasmac:cut-volts', 'qtplasmac.cut_volts-f', 'plasmac.cut-volts'])
        CALL(['halcmd', 'net', 'plasmac:kerf-width', 'qtplasmac.kerf_width-f', 'plasmac.kerf-width'])
        CALL(['halcmd', 'net', 'plasmac:pause-at-end', 'qtplasmac.pause_at_end-f', 'plasmac.pause-at-end'])
        CALL(['halcmd', 'net', 'plasmac:pierce-delay', 'qtplasmac.pierce_delay-f', 'plasmac.pierce-delay'])
        CALL(['halcmd', 'net', 'plasmac:pierce-height', 'qtplasmac.pierce_height-f', 'plasmac.pierce-height'])
        CALL(['halcmd', 'net', 'plasmac:puddle-jump-delay', 'qtplasmac.puddle_jump_delay-f', 'plasmac.puddle-jump-delay'])
        CALL(['halcmd', 'net', 'plasmac:puddle-jump-height', 'qtplasmac.puddle_jump_height-f', 'plasmac.puddle-jump-height'])
        CALL(['halcmd', 'net', 'plasmac:material-thickness', 'qtplasmac.material_thickness-f', 'plasmac.material-thickness'])
        # monitor
        CALL(['halcmd', 'net', 'plasmac:arc_ok_out', 'plasmac.arc-ok-out', 'qtplasmac.led_arc_ok'])
        CALL(['halcmd', 'net', 'plasmac:arc_voltage_out', 'plasmac.arc-voltage-out', 'qtplasmac.arc_voltage'])
        CALL(['halcmd', 'net', 'plasmac:breakaway-switch-out', 'qtplasmac.led_breakaway_switch'])
        CALL(['halcmd', 'net', 'plasmac:cornerlock-is-locked', 'plasmac.cornerlock-is-locked', 'qtplasmac.led_corner_lock'])
        CALL(['halcmd', 'net', 'plasmac:float-switch-out', 'qtplasmac.led_float_switch'])
        CALL(['halcmd', 'net', 'plasmac:voidlock-is-locked', 'plasmac.voidlock-is-locked', 'qtplasmac.led_void_lock'])
        CALL(['halcmd', 'net', 'plasmac:led-up', 'plasmac.led-up', 'qtplasmac.led_thc_up'])
        CALL(['halcmd', 'net', 'plasmac:led-down', 'plasmac.led-down', 'qtplasmac.led_thc_down'])
        CALL(['halcmd', 'net', 'plasmac:ohmic-probe-out', 'qtplasmac.ohmic_led_in'])
        CALL(['halcmd', 'net', 'plasmac:thc-active', 'plasmac.thc-active', 'qtplasmac.led_thc_active'])
        CALL(['halcmd', 'net', 'plasmac:thc-enabled', 'plasmac.thc-enabled', 'qtplasmac.led_thc_enabled'])
        CALL(['halcmd', 'net', 'plasmac:torch-on', 'qtplasmac.led_torch_on'])
        # control
        CALL(['halcmd', 'net', 'plasmac:cornerlock-enable', 'qtplasmac.cornerlock_enable', 'plasmac.cornerlock-enable'])
        CALL(['halcmd', 'net', 'plasmac:voidlock-enable', 'qtplasmac.voidlock_enable', 'plasmac.voidlock-enable'])
        CALL(['halcmd', 'net', 'plasmac:mesh-enable', 'qtplasmac.mesh_enable', 'plasmac.mesh-enable'])
        CALL(['halcmd', 'net', 'plasmac:ignore-arc-ok-1', 'qtplasmac.ignore_arc_ok', 'plasmac.ignore-arc-ok-1'])
        CALL(['halcmd', 'net', 'plasmac:ohmic-probe-enable', 'qtplasmac.ohmic_probe_enable', 'plasmac.ohmic-probe-enable'])
        CALL(['halcmd', 'net', 'plasmac:thc-enable', 'qtplasmac.thc_enable', 'plasmac.thc-enable'])
        CALL(['halcmd', 'net', 'plasmac:use-auto-volts', 'qtplasmac.use_auto_volts', 'plasmac.use-auto-volts'])
        CALL(['halcmd', 'net', 'plasmac:torch-enable', 'qtplasmac.torch_enable', 'plasmac.torch-enable'])
        # offsets
        CALL(['halcmd', 'net', 'plasmac:x-offset-current', 'qtplasmac.x_offset'])
        CALL(['halcmd', 'net', 'plasmac:y-offset-current', 'qtplasmac.y_offset'])
        CALL(['halcmd', 'net', 'plasmac:offsets-active', 'qtplasmac.offsets_active'])
        # override
        CALL(['halcmd', 'net', 'plasmac:height-override', 'qtplasmac.height_override', 'plasmac.height-override'])
        # INI
        CALL(['halcmd', 'net', 'plasmac:axis-x-max-limit', 'ini.x.max_limit', 'plasmac.axis-x-max-limit'])
        CALL(['halcmd', 'net', 'plasmac:axis-x-min-limit', 'ini.x.min_limit', 'plasmac.axis-x-min-limit'])
        CALL(['halcmd', 'net', 'plasmac:axis-y-max-limit', 'ini.y.max_limit', 'plasmac.axis-y-max-limit'])
        CALL(['halcmd', 'net', 'plasmac:axis-y-min-limit', 'ini.y.min_limit', 'plasmac.axis-y-min-limit'])
        CALL(['halcmd', 'net', 'plasmac:axis-z-max-limit', 'ini.z.max_limit', 'plasmac.axis-z-max-limit'])
        CALL(['halcmd', 'net', 'plasmac:axis-z-min-limit', 'ini.z.min_limit', 'plasmac.axis-z-min-limit'])
        # misc
        CALL(['halcmd', 'net', 'plasmac:consumable-changing', 'plasmac.consumable-changing', 'qtplasmac.consumable_changing'])
        CALL(['halcmd', 'net', 'plasmac:gcode-scale', 'plasmac.gcode-scale', 'qtplasmac.gcode_scale'])
        CALL(['halcmd', 'net', 'plasmac:jog-inhibit', 'qtplasmac.jog_inhibited'])
        CALL(['halcmd', 'net', 'plasmac:laser-on', 'qtplasmac.laser_on'])
        CALL(['halcmd', 'net', 'plasmac:laser-recovery-state', 'plasmac.laser-recovery-state', 'qtplasmac.laser_recovery_state'])
        CALL(['halcmd', 'net', 'plasmac:probe-test-error', 'plasmac.probe-test-error', 'qtplasmac.probe_test_error'])
        CALL(['halcmd', 'net', 'plasmac:sensor_active', 'plasmac.sensor-active', 'qtplasmac.sensor_active'])
        CALL(['halcmd', 'net', 'plasmac:state', 'plasmac.state-out', 'qtplasmac.plasmac_state'])
        CALL(['halcmd', 'net', 'plasmac:stop', 'plasmac.stop-type-out', 'qtplasmac.plasmac_stop'])
        CALL(['halcmd', 'net', 'plasmac:z-height', 'plasmac.z-height', 'qtplasmac.z_height'])
        CALL(['halcmd', 'net', 'plasmac:z-offset-counts', 'qtplasmac.z_offset_counts'])
        CALL(['halcmd', 'net', 'plasmac:offset-set-probe', 'plasmac.offset-set-probe', 'qtplasmac.offset_set_probe'])
        CALL(['halcmd', 'net', 'plasmac:offset-set-scribe', 'plasmac.offset-set-scribe', 'qtplasmac.offset_set_scribe'])
        CALL(['halcmd', 'net', 'plasmac:adaptive-feed', 'qtplasmac.vel_reduct'])

# *** add system hal pin changes here that may affect existing configs ***
# *** these may be removed after auto updating is implemented          ***
        if not hal.pin_has_writer('plasmac.feed-upm'):  # if feed-upm is not yet connected in hal
            CALL(['halcmd', 'net', 'plasmac:feed-upm', 'motion.feed-upm', 'plasmac.feed-upm'])

    def init_preferences(self):
        self.mode = self.PREFS.getpref('Mode', 0, int, 'GUI_OPTIONS')
        self.autorepeat_skip = self.PREFS.getpref('Autorepeat all', False, bool, 'GUI_OPTIONS')
        self.flash_error = self.PREFS.getpref('Flash error', False, bool, 'GUI_OPTIONS')
        self.w.chk_keyboard_shortcuts.setChecked(self.PREFS.getpref('Use keyboard shortcuts', False, bool, 'GUI_OPTIONS'))
        self.w.chk_soft_keyboard.setChecked(self.PREFS.getpref('Use soft keyboard', False, bool, 'GUI_OPTIONS'))
        self.w.chk_overlay.setChecked(self.PREFS.getpref('Show materials', True, bool, 'GUI_OPTIONS'))
        self.w.chk_run_from_line.setChecked(self.PREFS.getpref('Run from line', True, bool, 'GUI_OPTIONS'))
        self.w.chk_tool_tips.setChecked(self.PREFS.getpref('Tool tips', True, bool, 'GUI_OPTIONS'))
        self.w.chk_exit_warning.setChecked(self.PREFS.getpref('Exit warning', True, bool, 'GUI_OPTIONS'))
        self.exitMessage = self.PREFS.getpref('Exit warning text', '', str, 'GUI_OPTIONS')
        self.w.cone_size.setValue(self.PREFS.getpref('Preview cone size', 0.5, float, 'GUI_OPTIONS'))
        self.w.grid_size.setValue(self.PREFS.getpref('Preview grid size', 0.0, float, 'GUI_OPTIONS'))
        self.w.table_zoom_scale.setValue(self.PREFS.getpref('T view zoom scale', 1.0, float, 'GUI_OPTIONS'))
        self.zPlusOverrideJog = self.PREFS.getpref('Override jog inhibit via Z+', False, bool, 'GUI_OPTIONS')
        self.w.color_foregrnd.setStyleSheet(f'background-color: {self.PREFS.getpref("Foreground", "#ffee06", str, "COLOR_OPTIONS")}')
        self.w.color_foregalt.setStyleSheet(f'background-color: {self.PREFS.getpref("Highlight", "#ffee06", str, "COLOR_OPTIONS")}')
        self.w.color_led.setStyleSheet(f'background-color: {self.PREFS.getpref("LED", "#ffee06", str, "COLOR_OPTIONS")}')
        self.w.color_backgrnd.setStyleSheet(f'background-color: {self.PREFS.getpref("Background", "#16160e", str, "COLOR_OPTIONS")}')
        self.w.color_backgalt.setStyleSheet(f'background-color: {self.PREFS.getpref("Background Alt", "#36362e", str, "COLOR_OPTIONS")}')
        self.w.color_frams.setStyleSheet(f'background-color: {self.PREFS.getpref("Frames", "#ffee06", str, "COLOR_OPTIONS")}')
        self.w.color_estop.setStyleSheet(f'background-color: {self.PREFS.getpref("Estop", "#ff0000", str, "COLOR_OPTIONS")}')
        self.w.color_disabled.setStyleSheet(f'background-color: {self.PREFS.getpref("Disabled", "#b0b0b0", str, "COLOR_OPTIONS")}')
        self.w.color_preview.setStyleSheet(f'background-color: {self.PREFS.getpref("Preview", "#000000", str, "COLOR_OPTIONS")}')
        self.lastLoadedProgram = self.w.PREFS_.getpref('RecentPath_0', 'None', str, 'BOOK_KEEPING')
        TOOLTIPS.tool_tips_changed(self, self.w)
        self.soft_keyboard()
        self.cone_size_changed(self.w.cone_size.value())
        self.grid_size_changed(self.w.grid_size.value())
        self.set_basic_colors()
        self.w.sd_text.setText(self.exitMessage)

    def hide_widgets(self):
        for b in ['RUN', 'PAUSE', 'ABORT']:
            if self.PREFS.getpref(f'Hide {b.lower()}', False, bool, 'GUI_OPTIONS'):
                self.w[b.lower()].hide()
                if self.landscape:
                    self.w.machine_frame.setMaximumHeight(self.w.machine_frame.maximumHeight() - 44)
                    self.w.machine_frame.setMinimumHeight(self.w.machine_frame.maximumHeight())

    def init_widgets(self):
        droPos = self.PREFS.getpref('DRO position', 'bottom', str, 'GUI_OPTIONS')
        if droPos.lower() == 'top':
            # designer can change the layout name in the .ui file
            # this will find the name and move the dro to the top
            lay = self.w.dro_gcode_frame.children()[0].objectName()
            self.w[lay].removeWidget(self.w.dro_frame)
            self.w[lay].insertWidget(0, self.w.dro_frame)
        text = _translate('HandlerClass', 'CONTINUOUS')
        self.w.jogincrements.setItemText(0, text)
        self.w.main_tab_widget.setCurrentIndex(self.MAIN)
        self.w.preview_stack.setCurrentIndex(self.PREVIEW)
        self.prevPreviewIndex = self.PREVIEW
        self.w.gcode_stack.setCurrentIndex(self.GCODE)
        self.w.jog_stack.setCurrentIndex(self.JOG)
        self.w.thc_enable.setChecked(self.PREFS.getpref('THC enable', True, bool, 'ENABLE_OPTIONS'))
        self.w.cornerlock_enable.setChecked(self.PREFS.getpref('Corner lock enable', True, bool, 'ENABLE_OPTIONS'))
        self.w.voidlock_enable.setChecked(self.PREFS.getpref('Void lock enable', False, bool, 'ENABLE_OPTIONS'))
        self.w.use_auto_volts.setChecked(self.PREFS.getpref('Use auto volts', True, bool, 'ENABLE_OPTIONS'))
        self.w.ohmic_probe_enable.setChecked(self.PREFS.getpref('Ohmic probe enable', False, bool, 'ENABLE_OPTIONS'))
        self.w.thc_auto.setChecked(self.PREFS.getpref('THC auto', False, bool, 'ENABLE_OPTIONS'))
        self.thc_auto_changed(self.w.thc_auto.isChecked())
        self.w.error_label = QLabel()
        self.w.tool_label = STATLABEL()
        self.w.gcodes_label = STATLABEL()
        self.w.mcodes_label = STATLABEL()
        self.w.lbl_tool = STATLABEL()
        self.w.lbl_gcodes = STATLABEL()
        self.w.lbl_mcodes = STATLABEL()
        self.w.statusbar.addPermanentWidget(self.w.error_label, stretch=1)
        self.w.error_label.setObjectName('error_label')
        self.w.statusbar.addPermanentWidget(VLine())
        self.w.statusbar.addPermanentWidget(self.w.tool_label)
        self.w.tool_label.setObjectName('tool_label')
        self.w.tool_label.setText('TOOL: ')
        self.w.statusbar.addPermanentWidget(self.w.lbl_tool)
        self.w.lbl_tool.setObjectName('lbl_tool')
        self.w.statusbar.addPermanentWidget(VLine())
        self.w.statusbar.addPermanentWidget(self.w.gcodes_label)
        self.w.gcodes_label.setObjectName('gcodes_label')
        self.w.gcodes_label.setText('G-CODES: ')
        self.w.statusbar.addPermanentWidget(self.w.lbl_gcodes)
        self.w.lbl_gcodes.setObjectName('lbl_gcodes')
        self.w.statusbar.addPermanentWidget(VLine())
        self.w.statusbar.addPermanentWidget(self.w.mcodes_label)
        self.w.mcodes_label.setObjectName('mcodes_label')
        self.w.mcodes_label.setText('M-CODES: ')
        self.w.statusbar.addPermanentWidget(self.w.lbl_mcodes)
        self.w.lbl_mcodes.setObjectName('lbl_mcodes')
        text = _translate('HandlerClass', 'MOVE')
        self.w.cut_rec_move_label.setText(f'{text}\n{self.w.kerf_width.text()}')
        self.w.filemanager.windowLayout.setContentsMargins(4, 0, 4, 0)
        self.w.filemanager.windowLayout.setSpacing(4)
        # hide load button (redundant to select button)
        self.w.filemanager.loadButton.hide()
        # for copy/paste control if required
        self.w.filemanager.copy_control.hide()
        # add vertical and horizontal scroll bars to the materials QComboBoxs
        self.w.material_selector.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.material_selector.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.conv_material.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.conv_material.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.materials_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.materials_box.view().setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.w.gcode_display.set_margin_metric(3)
        self.w.gcode_display.setBraceMatching(False)
        self.w.gcode_display.setCaretWidth(0)
        self.w.gcode_display.setCornerWidget(QLabel())
        self.w.gcode_editor.topBox.setContentsMargins(4, 4, 4, 4)
        self.w.gcode_editor.bottomBox.setContentsMargins(4, 4, 4, 4)
        self.w.gcode_editor.set_margin_metric(3)
        self.w.gcode_editor.editor.setBraceMatching(False)
        self.w.gcode_editor.editor.setCaretWidth(4)
        self.w.gcode_editor.editor.setCornerWidget(QLabel())
        self.w.gcode_editor.editMode()
        self.w.gcode_editor.pythonLexerAction.setVisible(False)
        self.w.gcode_editor.gCodeLexerAction.setVisible(False)
        self.w.gcode_editor.label.setText('')
        self.w.gcodegraphics.enable_dro = True
        self.w.gcodegraphics.set_alpha_mode(True)
        self.w.gcodegraphics.setShowOffsets(False)
        self.w.gcodegraphics._font = 'monospace 11'
        self.w.conv_preview.enable_dro = True
        self.w.conv_preview.set_cone_basesize(0.1)
        self.w.conv_preview.set_view('Z')
        self.w.conv_preview.show_tool = False
        self.w.conv_preview.show_limits = False
        self.w.conv_preview.show_small_origin = False
        self.w.conv_preview.set_alpha_mode(True)
        self.w.conv_preview.setShowOffsets(False)
        self.w.conv_preview._font = 'monospace 11'
        self.w.conv_preview.inhibit_selection = True
        self.w.conv_preview.update()
        self.w.conv_preview.setInhibitControls(True)
        self.w.estopButton = self.PREFS.getpref('Estop type', 0, int, 'GUI_OPTIONS')
        if self.w.estopButton == 0:
            self.w.estop.setEnabled(False)
        if self.w.estopButton == 1:
            self.w.estop.hide()
        self.w.camview.cross_color = QtCore.Qt.red
        self.w.camview.cross_pointer_color = QtCore.Qt.red
        self.w.camview.font = QFont('arial,helvetica', 16)
        self.flashState = False
        self.flashRate = 4
        self.flasher = self.flashRate
        self.manualCut = False
        self.jogPreManCut = [False, INFO.DEFAULT_LINEAR_JOG_VEL, 0]
        self.probeTest = False
        self.torchPulse = False
        self.rflSelected = False
        self.fileOpened = False
        self.fileClear = False
        self.error_present = False
        self.laserButtonState = 'laser'
        self.camButtonState = 'markedge'
        self.overlayProgress = QProgressBar(self.w.gcode_display)
        self.overlayProgress.setOrientation(Qt.Vertical)
        self.overlayProgress.setInvertedAppearance(True)
        self.overlayProgress.setFormat('')
        self.overlayProgress.hide()
        self.camNum = self.PREFS.getpref('Camera port', 0, int, 'CAMERA_OFFSET')

    def get_overlay_text(self):
        text = ['']
        if not self.w.chk_overlay.isChecked():
            return text
        scale = 1
        if self.units == 'in' and self.w.dro_z.display_units_mm:
            scale = 25.4
        elif self.units == 'mm' and not self.w.dro_z.display_units_mm:
            scale = 1 / 25.4
        if not self.w.dro_z.display_units_mm:
            fr = 1
            ou = 3
        else:
            fr = 0
            ou = 2
        dp = '.' if '.' in self.w.pierce_height.text() else ','
        if '.' in self.w.cut_feed_rate.text().replace(",", ".") and len(self.w.cut_feed_rate.text().replace(",", ".").split('.')[0]) > 3:
            text.append(f'FR: {float(self.w.cut_feed_rate.text().replace(",", ".").split(".")[0]) * scale:.{fr}f}'.replace(".", dp))
        else:
            text.append(f'FR: {float(self.w.cut_feed_rate.text().replace(",", ".")) * scale:.{fr}f}'.replace(".", dp))
        text.append(f'PH: {float(self.w.pierce_height.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp))
        text.append(f'PD: {self.w.pierce_delay.text()}')
        text.append(f'CH: {float(self.w.cut_height.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp))
        text.append(f'KW: {float(self.w.kerf_width.text().replace(",", ".")) * scale:.{ou}f}'.replace(".", dp))
        if self.pmx485Exists:
            text.append(f'CA: {self.w.cut_amps.text()}')
        return text

    def offset_peripherals(self):
        self.camOffsetX = 0.0
        self.camOffsetY = 0.0
        self.laserOffsetX = 0.0
        self.laserOffsetY = 0.0
        self.probeOffsetX = 0.0
        self.probeOffsetY = 0.0
        self.probeDelay = 0.0
        head = _translate('HandlerClass', 'Prefs File Error')
        # laser
        try:
            self.laserOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'LASER_OFFSET')
            self.laserOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'LASER_OFFSET')
        except:
            self.w.laser.hide()
            msg0 = _translate('HandlerClass', 'Invalid entry for laser offset')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
        if not (self.laserOffsetX or self.laserOffsetY):
            self.w.laser.hide()
        # camera
        try:
            self.camOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'CAMERA_OFFSET')
            self.camOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'CAMERA_OFFSET')
        except:
            self.w.camera.hide()
            msg0 = _translate('HandlerClass', 'Invalid entry for camera offset')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
        if self.camOffsetX or self.camOffsetY:
            self.w.camview.set_camnum(self.camNum)
        else:
            self.w.camera.hide()
        # probing
        try:
            self.probeOffsetX = self.PREFS.getpref('X axis', 0.0, float, 'OFFSET_PROBING')
            self.probeOffsetY = self.PREFS.getpref('Y axis', 0.0, float, 'OFFSET_PROBING')
            self.probeDelay = self.PREFS.getpref('Delay', 0.0, float, 'OFFSET_PROBING')
        except:
            self.w.offset_feed_rate.hide()
            self.w.offset_feed_rate_lbl.hide()
            msg0 = _translate('HandlerClass', 'Invalid entry for probe offset')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
        if not self.probeOffsetX and not self.probeOffsetY:
            self.w.offset_feed_rate.hide()
            self.w.offset_feed_rate_lbl.hide()

# called by the modified closeEvent function in this handler
    def closing_cleanup__(self):
        # disconnect powermax
        self.w.pmx485_enable.setChecked(False)
        # close soft keyboard
        self.vkb_hide()
        # turn autorepeat back on for the OS
        self.autorepeat_keys(True)
        # save the log files
        self.save_logfile(5)
        # save preferences
        if not self.PREFS:
            return
        self.PREFS.putpref('Use keyboard shortcuts', self.w.chk_keyboard_shortcuts.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Use soft keyboard', self.w.chk_soft_keyboard.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Show materials', self.w.chk_overlay.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Run from line', self.w.chk_run_from_line.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Tool tips', self.w.chk_tool_tips.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Exit warning', self.w.chk_exit_warning.isChecked(), bool, 'GUI_OPTIONS')
        self.PREFS.putpref('Exit warning text', self.exitMessage, str, 'GUI_OPTIONS')
        self.PREFS.putpref('Preview cone size', self.w.cone_size.value(), float, 'GUI_OPTIONS')
        self.PREFS.putpref('Preview grid size', self.w.grid_size.value(), float, 'GUI_OPTIONS')
        self.PREFS.putpref('T view zoom scale', self.w.table_zoom_scale.value(), float, 'GUI_OPTIONS')
        self.PREFS.putpref('THC auto', self.w.thc_auto.isChecked(), bool, 'ENABLE_OPTIONS')
        self.PREFS.putpref('Override jog inhibit via Z+', self.zPlusOverrideJog, bool, 'GUI_OPTIONS')
        self.PREFS.putpref('THC enable', self.w.thc_enable.isChecked(), bool, 'ENABLE_OPTIONS')
        self.PREFS.putpref('Corner lock enable', self.w.cornerlock_enable.isChecked(), bool, 'ENABLE_OPTIONS')
        self.PREFS.putpref('Void lock enable', self.w.voidlock_enable.isChecked(), bool, 'ENABLE_OPTIONS')
        self.PREFS.putpref('Use auto volts', self.w.use_auto_volts.isChecked(), bool, 'ENABLE_OPTIONS')
        self.PREFS.putpref('Ohmic probe enable', self.w.ohmic_probe_enable.isChecked(), bool, 'ENABLE_OPTIONS')

    def save_logfile(self, numLogs):
        logPre = 'machine_log_'
        logFiles = []
        logFiles = [f for f in os.listdir(self.PATHS.CONFIGPATH) if f.startswith(logPre)]
        logFiles.sort()
        if len(logFiles) > (numLogs - 1):
            for f in range(0, len(logFiles) - (numLogs - 1)):
                os.remove(logFiles[0])
                logFiles = logFiles[1:]
        text = self.w.machinelog.getLogText()
        logName = f'{self.PATHS.CONFIGPATH}/{logPre}{time.strftime("%y-%m-%d_%H-%M-%S")}.txt'
        with open(logName, 'w') as f:
            f.write(text)

# called by qt_makegui.py
    def processed_key_event__(self, receiver, event, is_pressed, key, code, shift, cntrl):
        # when typing in MDI, we don't want keybinding to call functions
        # so we catch and process the events directly.
        # We do want ESC, F1 and F2 to call keybinding functions though
        if code not in (Qt.Key_Escape, Qt.Key_F1, Qt.Key_F2):
            # search for the top widget of whatever widget received the event
            # then check if it's one we want the keypress events to go to
            flag = False
            allowTab = False
            mdiBlank = False
            receiver2 = receiver
            while receiver2 is not None and not flag:
                if isinstance(receiver2, QtWidgets.QDialog):
                    flag = True
                    break
                if isinstance(receiver2, QtWidgets.QListView):
                    flag = True
                    break
                if isinstance(receiver2, FILE_MAN):
                    flag = True
                    break
                if isinstance(receiver2, MDI_LINE):
                    if self.w.mdihistory.MDILine.text().rstrip() == '':
                        mdiBlank = True
                    flag = True
                    break
                if isinstance(receiver2, MDI_HISTORY):
                    if self.w.mdihistory.MDILine.text().rstrip() == '':
                        mdiBlank = True
                    flag = True
                    break
                if isinstance(receiver2, EDITOR):
                    flag = True
                    break
                if isinstance(receiver2, DOUBLESCALE):
                    flag = True
                    break
                if isinstance(receiver2, OFFSETVIEW):
                    flag = True
                    break
                if self.w.main_tab_widget.currentIndex() not in (self.MAIN, self.STATISTICS):
                    allowTab = True
                    flag = True
                    break
                receiver2 = receiver2.parent()
            if flag:
                if is_pressed:
                    if allowTab and (code == Qt.Key_Tab or code == Qt.Key_BackTab):
                        self.keyPressEvent(event)
                    else:
                        receiver.keyPressEvent(event)
                    if mdiBlank and (code == Qt.Key_Return or code == Qt.Key_Enter):
                        self.keyPressEvent(event)
                    event.accept()
                    return True
                else:
                    event.accept()
                    return True
        if event.isAutoRepeat():
            return True
        return KEYBIND.manage_function_calls(self, event, is_pressed, key, shift, cntrl)

    def size_changed(self, object):
        bHeight = self.w.file_edit.height() + 4
        rows = int((object.height() - 24) / bHeight)
        if self.landscape:
            if rows == 4:
                self.buttons_hide(7, 20)
            elif rows == 5:
                self.buttons_hide(9, 20)
                self.buttons_show(7, 8)
            elif rows == 6:
                self.buttons_hide(11, 20)
                self.buttons_show(7, 10)
            elif rows == 7:
                self.buttons_hide(13, 20)
                self.buttons_show(7, 12)
            elif rows == 8:
                self.buttons_hide(15, 20)
                self.buttons_show(7, 14)
            elif rows == 9:
                self.buttons_hide(17, 20)
                self.buttons_show(7, 16)
            elif rows == 10:
                self.buttons_hide(19, 20)
                self.buttons_show(7, 18)
            else:
                self.buttons_show(7, 20)
        else:
            if rows == 15:
                self.buttons_hide(16, 20)
                self.buttons_show(9, 15)
            if rows == 16:
                self.buttons_hide(17, 20)
                self.buttons_show(9, 16)
            elif rows == 17:
                self.buttons_hide(18, 20)
                self.buttons_show(9, 17)
            elif rows == 18:
                self.buttons_hide(19, 20)
                self.buttons_show(9, 18)
            elif rows == 19:
                self.buttons_hide(19, 20)
                self.buttons_show(9, 19)
            else:
                self.buttons_show(9, 20)

    def buttons_hide(self, first, last):
        for b in range(first, last + 1):
            self.w[f'button_{b}'].hide()

    def buttons_show(self, first, last):
        for b in range(first, last + 1):
            self.w[f'button_{b}'].show()

#########################################################################################################################
# CALLBACKS FROM STATUS #
#########################################################################################################################

    def estop_state(self, state):
        if state:
            self.w.power.setChecked(False)
            if not self.firstRun:
                log = _translate('HandlerClass', 'Emergency stop pressed')
                STATUS.emit('update-machine-log', log, 'TIME')
        else:
            if not self.firstRun:
                log = _translate('HandlerClass', 'Emergency stop cleared')
                STATUS.emit('update-machine-log', log, 'TIME')

    def power_state(self, state):
        if state:
            self.w.power.setChecked(True)
            if not self.firstRun:
                log = _translate('HandlerClass', 'GUI power on')
                STATUS.emit('update-machine-log', log, 'TIME')
        else:
            self.w.power.setChecked(False)
            if self.tpButton and self.torchPulse:
                self.torch_pulse_abort()
            if self.ptButton and hal.get_value('plasmac.probe-test'):
                self.probe_test_stop()
                log = _translate('HandlerClass', 'Probe test aborted due to GUI power off')
                STATUS.emit('update-machine-log', log, 'TIME')
            if not self.firstRun:
                log = _translate('HandlerClass', 'GUI power off')
                STATUS.emit('update-machine-log', log, 'TIME')
        self.refresh_button_states()

    def interp_idle(self, obj):
        hal.set_p('plasmac.consumable-change', '0')
        if self.single_cut_request:
            self.single_cut_request = False
            if self.oldFile and self.fileOpened:
                self.remove_temp_materials()
                ACTION.OPEN_PROGRAM(self.oldFile)
            else:
                self.fileOpened = True
                self.file_clear_clicked()
            if self.g91:
                ACTION.CALL_MDI_WAIT('G91')
        self.w.jog_stack.setCurrentIndex(self.JOG)
        if self.ccButton and not self.button_normal_check(self.ccButton):
            self.button_normal(self.ccButton)
        if self.laserDryRunCoords:
                oldX = STATUS.get_position()[0][0] - self.laserDryRunCoords[0]
                oldY = STATUS.get_position()[0][1] - self.laserDryRunCoords[1]
                units = '20' if self.gcodeProps['gcode_units'] == 'in' else '21'
                ACTION.CALL_MDI_WAIT(f'G{units} G10 L20 P0 X{oldX / self.boundsMultiplier} Y{oldY / self.boundsMultiplier}')
                self.laserDryRunCoords = None
                self.laserOnPin.set(0)
                hal.set_p('plasmac.dry-run', '0')
                self.w.gcodegraphics.logger.clear()
        if self.framing:
                self.framing = False
                self.laserOnPin.set(0)
                self.w.gcodegraphics.logger.clear()
        if self.w.preview_stack.currentIndex() != self.PREVIEW:
            self.preview_stack_changed()
        if self.jobRunning and obj:
            if self.w.torch_enable.isChecked():
                self.statistics_save()
            else:
                self.jobRunning = False
            log = _translate('HandlerClass', 'Machine idle')
            STATUS.emit('update-machine-log', log, 'TIME')
            self.statistics_show()
        ACTION.SET_MANUAL_MODE()
        self.refresh_button_states()

    def interp_paused(self, obj):
        pass

    def interp_running(self, obj):
        if STATUS.is_auto_mode() and self.w.gcode_stack.currentIndex() != self.GCODE:
            self.w.gcode_stack.setCurrentIndex(self.GCODE)
        self.refresh_button_states()

    def interp_reading(self, obj):
        pass

    def interp_waiting(self, obj):
        pass

    def pause_changed(self, obj, state):
        if state:
            log = _translate('HandlerClass', 'Cycle paused')
            STATUS.emit('update-machine-log', log, 'TIME')
        else:
            self.w.jog_stack.setCurrentIndex(self.JOG)
        self.refresh_button_states()

    def jog_rate_changed(self, object, value):
        msg0 = _translate('HandlerClass', 'JOG')
        self.w.jogs_label.setText(f'{msg0}\n{STATUS.get_jograte():.0f}')

    def progress_changed(self, object, percent):
        if percent < 0:
            self.overlayProgress.setValue(0)
            self.overlayProgress.hide()
            self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self.w.gcode_display.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        else:
            self.overlayProgress.setValue(percent)
            self.overlayProgress.show()
            self.overlayProgress.setFixedHeight(self.w.gcode_display.geometry().height())
            self.overlayProgress.move(self.w.gcode_display.geometry().width() - 20, 0)
            self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.w.gcode_display.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def user_system_changed(self, obj, data):
        userSys = self.systemList[int(data)]
        self.w.wcs_button.setText(f'WCS\n{userSys}')
        if ACTION.prefilter_path:
            self.file_reload_clicked()

    def file_loaded(self, obj, filename):
        if filename is not None:
            if 'qtplasmac_file_clear.ngc' in filename:
                self.fileClear = True
            self.overlayProgress.setValue(0)
            if not any(name in filename for name in ['qtplasmac_file_clear.ngc', 'single_cut.ngc']):
                self.lastLoadedProgram = filename
            if not self.cameraOn:
                self.preview_index_return(self.w.preview_stack.currentIndex())
            self.w.file_open.setText(os.path.basename(filename))
            if not self.single_cut_request:
                self.fileOpened = True
            text = _translate('HandlerClass', 'EDIT')
            self.w.edit_label.setText(f'{text}: {filename}')
            if self.w.gcode_stack.currentIndex() != self.GCODE:
                self.w.gcode_stack.setCurrentIndex(self.GCODE)
        self.w.gcodegraphics.logger.clear()
        self.w.gcodegraphics.clear_highlight()
        if self.preRflFile and self.preRflFile != ACTION.prefilter_path:
            self.rflActive = False
            self.startLine = 0
            self.preRflFile = ''
        self.fileBoundsError, fileErrMsg = self.bounds_check_file()
        self.probeBoundsError, probeErrMsg = self.bounds_check_probe(False)
        errMsg = fileErrMsg + probeErrMsg
        if self.fileBoundsError or self.probeBoundsError:
            head = _translate('HandlerClass', 'Axis Limit Error')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n')
            if self.single_cut_request:
                self.single_cut_request = False
                if self.oldFile and 'single_cut' not in self.oldFile:
                    self.remove_temp_materials()
                    ACTION.OPEN_PROGRAM(self.oldFile)
                else:
                    # allows file clear to work on error
                    self.fileOpened = True
        else:
            if self.single_cut_request:
                ACTION.RUN()
            if self.fileClear:
                self.fileClear = False
                self.fileOpened = False
                text = _translate('HandlerClass', 'OPEN')
                self.w.file_open.setText(text)
                text = _translate('HandlerClass', 'EDIT')
                self.w.edit_label.setText(f'{text}')
                self.w.gcode_editor.editor.new_text()
                self.w.gcode_editor.editor.setModified(False)
                self.w.gcode_display.new_text()
                self.w.gcode_display.set_margin_metric(3)
                self.w.gcode_editor.set_margin_metric(3)
        ACTION.SET_MANUAL_MODE()
        if not len(self.w.gcode_display.text()):
            self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        if self.w.main_tab_widget.currentIndex() != self.MAIN:
            self.w.main_tab_widget.setCurrentIndex(self.MAIN)
        # forces the view to remain "table view" if T is checked when a file is loaded, or change to table view upon clicking CLEAR
        if self.w.view_t.isChecked() or 'qtplasmac_file_clear.ngc' in filename:
            self.view_t_pressed(self.w.gcodegraphics)
        if 'single_cut.ngc' not in filename:
            self.preSingleCutMaterial = None
        # remove unused temporary materials from comboboxes
        self.getMaterialBusy = True
        if int(self.w.materials_box.currentText().split(": ", 1)[0]) not in self.materialList:
            self.change_material(self.defaultMaterial)
        for idx in range(self.w.materials_box.count() - 1, -1, -1):
            matNum = int(self.w.materials_box.itemText(idx).split(': ', 1)[0])
            if matNum < 1000000:
                break
            if matNum not in self.materialList:
                self.w.materials_box.removeItem(idx)
                self.w.material_selector.removeItem(idx)
                self.w.conv_material.removeItem(idx)
        self.getMaterialBusy = False
        self.refresh_button_states()

    def joints_all_homed(self, obj):
        self.interp_idle(None)
        if not self.firstHoming:
            ACTION.CALL_MDI_WAIT('T0 M6')
            ACTION.SET_MANUAL_MODE()
            self.firstHoming = True
        self.w.gcodegraphics.update()
        self.w.conv_preview.update()
        log = _translate('HandlerClass', 'Machine homed')
        STATUS.emit('update-machine-log', log, 'TIME')

    def joint_homed(self, obj, joint):
        dro = self.coordinates[int(joint)]
        self.w[f'dro_{dro}'].setProperty('homed', True)
        self.w[f'dro_{dro}'].setStyle(self.w[f'dro_{dro}'].style())
        self.w[f'dro_label_{dro}'].setProperty('homed', True)
        self.w[f'dro_label_{dro}'].setStyle(self.w[f'dro_label_{dro}'].style())
        self.w.update
        STATUS.emit('dro-reference-change-request', 1)
        self.w.gcodegraphics.logger.clear()

    def joint_unhomed(self, obj, joints):
        for joint in joints:
            dro = self.coordinates[int(joint)]
            self.w[f'dro_{dro}'].setProperty('homed', False)
            self.w[f'dro_{dro}'].setStyle(self.w[f'dro_{dro}'].style())
            self.w[f'dro_label_{dro}'].setProperty('homed', False)
            self.w[f'dro_label_{dro}'].setStyle(self.w[f'dro_label_{dro}'].style())
        if len(joints) < len(self.coordinates):
            self.w.home_all.setEnabled(True)
        self.w.update
        STATUS.emit('dro-reference-change-request', 1)
        self.interp_idle(None)
        self.w.gcodegraphics.update()
        self.w.conv_preview.update()

    def hard_limit_tripped(self, obj, tripped, list_of_tripped):
        self.w.chk_override_limits.setEnabled(tripped)
        if not tripped:
            self.w.chk_override_limits.setChecked(False)

    def jog_inhibited_changed(self, state):
        if state:
            self.w.chk_override_jog.setEnabled(state)
            self.w.releaseKeyboard()
        else:
            if not self.w.led_float_switch.hal_pin.get() and not self.ohmicLedInPin.get() and \
                                                            not self.w.led_breakaway_switch.hal_pin.get():
                self.w.chk_override_jog.setChecked(False)
                hal.set_p('plasmac.override-jog', '0')

    def sensor_active_changed(self, state):
        if not state:
            self.w.chk_override_jog.setEnabled(False)
            self.w.chk_override_jog.setChecked(False)
            hal.set_p('plasmac.override-jog', str(state))

    def z_offset_changed(self, height):
        currentState = self.plasmacStatePin.get()
        if currentState in [self.PROBE_HEIGHT, self.PROBE_DOWN, self.PROBE_UP,
                            self.ZERO_HEIGHT, self.PIERCE_HEIGHT, self.PUDDLE_JUMP,
                            self.CUT_HEIGHT, self.SAFE_HEIGHT, self.MAX_HEIGHT]:
            self.watchDogState = currentState
            self.watchDogTimer.start(3000)
        if STATUS.is_interp_paused() and not height and \
            (hal.get_value('plasmac.stop-type-out') or hal.get_value('plasmac.cut-recovering')):
                self.w.set_cut_recovery()

    def override_jog_changed(self, state):
        if state:
            hal.set_p('plasmac.override-jog', '1')
        else:
            hal.set_p('plasmac.override-jog', '0')

    def tool_changed(self, obj, tool):
        if tool == 0:
            self.tool = self.TORCH
            text = _translate('HandlerClass', 'TORCH')
            self.w.lbl_tool.setText(text)
        elif tool == 1:
            self.tool = self.SCRIBE
            text = _translate('HandlerClass', 'SCRIBE')
            self.w.lbl_tool.setText(text)
        else:
            self.tool = self.UNKNOWN
            text = _translate('HandlerClass', 'UNKNOWN')
            self.w.lbl_tool.setText(text)

    def gcodes_changed(self, obj, cod):
        if self.units == 'in' and STATUS.is_metric_mode():
            self.droScale = 25.4
            self.gcodeScalePin.set(25.4)
        elif self.units == 'mm' and not STATUS.is_metric_mode():
            self.droScale = 1 / 25.4
            self.gcodeScalePin.set(1 / 25.4)
        else:
            self.droScale = 1
            self.gcodeScalePin.set(1)
        self.w.lbl_gcodes.setText(f'{cod}')

    def mcodes_changed(self, obj, cod):
        self.w.lbl_mcodes.setText(f'{cod}')

    def metric_mode_changed(self, obj, state):
        self.w.gcodegraphics.update()

    def set_start_line(self, line):
        if self.fileOpened:
            if self.w.chk_run_from_line.isChecked():
                if self.w.sender():
                    if self.w.sender().objectName() == 'gcode_editor_display':
                        return
                if line > 1:
                    msg0 = _translate('HandlerClass', 'SELECTED')
                    self.w.run.setText(f'{msg0} {line}')
                    self.runText = f'{msg0} {line}'
                    self.rflSelected = True
                    self.startLine = line - 1
                elif self.rflActive:
                    txt0 = _translate('HandlerClass', 'RUN FROM LINE')
                    txt1 = _translate('HandlerClass', 'CYCLE START')
                    self.runText = f'{txt0}\n{txt1}'
                    self.rflSelected = False
                else:
                    self.startLine = 0
                    self.rflSelected = False
                    self.w.gcodegraphics.clear_highlight()
            if line < 1:
                self.w.gcode_display.setCursorPosition(0, 0)
                self.w.gcode_display.moveMarker(0)

    def update_gcode_properties(self, props):
        if props:
            self.gcodeProps = props
            if props['gcode_units'] == 'in':
                STATUS.emit('metric-mode-changed', False)
            else:
                STATUS.emit('metric-mode-changed', True)

    def error_update(self, obj, kind, error):
        if not self.realTimeDelay:
            if kind == linuxcnc.OPERATOR_ERROR:
                self.error_status(True)
                self.mdiError = True
            if kind == linuxcnc.NML_ERROR:
                self.error_status(True)

#########################################################################################################################
# CALLBACKS FROM FORM #
#########################################################################################################################

    def ext_run(self, state):
        if self.w.run.isEnabled() and state:
            self.run_clicked()

    def ext_abort(self, state):
        if self.w.abort.isEnabled() and state:
            self.abort_pressed()

    def ext_pause(self, state):
        if self.w.pause_resume.isEnabled() and state:
            if STATUS.stat.paused:
                self.pause_resume_pressed()
            ACTION.PAUSE()

    def ext_pause_only(self, state):
        if self.w.pause_resume.isEnabled() and state:
            ACTION.PAUSE_MACHINE()

    def ext_resume(self, state):
        if self.w.pause_resume.isEnabled() and state:
            self.pause_resume_pressed()
            ACTION.RESUME()

    def ext_touch_off(self, state):
        if self.w.touch_xy.isEnabled() and state:
            self.touch_xy_clicked()

    def ext_laser_touch_off(self, state):
        if self.w.laser.isEnabled():
            if state:
                self.extLaserButton = True
                self.laser_pressed()
            else:
                self.extLaserButton = False
                self.laser_clicked()

    def ext_laser_toggle(self, state):
        if self.w.laser.isEnabled() and state:
            self.laserOnPin.set(not self.laserOnPin.get())

    def ext_jog_slow(self, state):
        if self.w.jog_slow.isEnabled() and state:
            self.jog_slow_pressed(True)

    def ext_run_pause(self, state):
        if self.w.run.isEnabled() and state:
            self.run_clicked()
        elif self.w.pause_resume.isEnabled() and state:
            if STATUS.stat.paused:
                self.pause_resume_pressed()
            ACTION.PAUSE()

    def power_button(self, action, state):
        if action == 'pressed':
            self.shutdownTimer.start(self.shutdownTime)
        elif action == 'released':
            self.shutdownTimer.stop()
        elif action == 'clicked':
            if STATUS.estop_is_clear():
                ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on())
            else:
                self.w.power.setChecked(False)
        elif action == 'external' and self.w.power.isEnabled() and not self.firstRun:
            if state:
                self.shutdownTimer.start(self.shutdownTime)
                self.w.power.setDown(True)
            else:
                self.shutdownTimer.stop()
                self.w.power.setDown(False)
                self.w.power.click()

    def run_clicked(self):
        if self.startLine and self.rflSelected:
            self.rflActive = True
            self.refresh_button_states()
            self.rflSelected = False
            if self.developmentPin.get():
                reload(RFL)
            self.runType = self.dialog_rfl_type()
            if self.runType['cancel']:
                if 'rfl.ngc' in self.lastLoadedProgram:
                    self.set_start_line(-1)
                else:
                    self.clear_rfl()
                return
            lastLine = 0
            if self.runType['type'] == 'cut':
                count = 0
                start = 0
                start_found = False
                in_keep_z_motion = False
                with open(self.lastLoadedProgram, 'r') as inFile:
                    for line in inFile:
                        stripped = line.replace(' ', '')
                        # track keep-z-motion state
                        if '#<keep-z-motion>=1' in stripped:
                            in_keep_z_motion = True
                        elif '#<keep-z-motion>=0' in stripped:
                            in_keep_z_motion = False
                        # find start: last G00 XY before or first G00 XY after startLine
                        if line[:3] == 'G00' and 'G53' not in line and ('X' in line or 'Y' in line):
                            if count <= self.startLine or not start_found:
                                start = count
                                start_found = (count >= self.startLine)
                        # find end: after both startLine and start
                        if count > max(self.startLine, start):
                            if (in_keep_z_motion and (('G53' in line and 'G00' in line and 'Z' in line) or '#<keep-z-motion>=0' in stripped)) or \
                            (not in_keep_z_motion and line[:3] == 'M05'):
                                break
                        count += 1
                lastLine = count
                self.startLine = start
            head = _translate('HandlerClass', 'Gcode Error')
            data = RFL.run_from_line_get(self.lastLoadedProgram, self.startLine, lastLine)
            # cannot do run from line within a subroutine or if using cutter compensation
            if data['error']:
                if data['compError']:
                    msg0 = _translate('HandlerClass', 'Cannot run from line while')
                    msg1 = _translate('HandlerClass', 'cutter compensation is active')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
                if data['subError']:
                    msg0 = _translate('HandlerClass', 'Cannot do run from line')
                    msg1 = _translate('HandlerClass', 'inside subroutine')
                    msg2 = ''
                    for sub in data['subError']:
                        msg2 += f' {sub}'
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}{msg2}\n')
                self.clear_rfl()
            else:
                # are we in a cutting operation (needs leadin dialog)
                is_cutting = (data['codes']['spindle']['line'] and
                              'M03' in data['codes']['spindle']['line'] and
                              '$0' in data['codes']['spindle']['line'] and not
                              data.get('keep_z_motion_active', False))
                # skip leadin dialog for "this cutpath" or non-cutting operations
                if self.runType['type'] == 'cut' or not is_cutting:
                    # fake user input
                    userInput = {'cancel': False, 'do': False, 'length': 0, 'angle': 0}
                # get user input
                else:
                    userInput = self.dialog_run_from_line()
                    # rfl cancel clicked
                    if userInput['cancel']:
                        if 'rfl.ngc' in self.lastLoadedProgram:
                            self.set_start_line(-1)
                        else:
                            self.clear_rfl()
                        return
                # rfl load clicked
                rflFile = f'{self.tmpPath}rfl.ngc'
                result = RFL.run_from_line_set(rflFile, data, userInput, self.unitsPerMm)
                # leadin cannot be used
                if result['error']:
                    msg0 = _translate('HandlerClass', 'Unable to calculate a leadin for this cut')
                    msg1 = _translate('HandlerClass', 'Program will run from selected line with no leadin applied')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
                # load rfl file
                if ACTION.prefilter_path or self.lastLoadedProgram != 'None':
                    self.preRflFile = ACTION.prefilter_path or self.lastLoadedProgram
                ACTION.OPEN_PROGRAM(rflFile)
                ACTION.prefilter_path = self.preRflFile
                if self.runType['type'] == 'cut':
                    log = _translate('HandlerClass', 'Run from line - this cutpath loaded')
                else:
                    log = _translate('HandlerClass', 'Run from line - here to end loaded')
                log1 = _translate('HandlerClass', 'Start line')
                STATUS.emit('update-machine-log', f'{log} - {log1}: {(self.startLine + 1)}', 'TIME')
        elif self.cut_critical_toggle_check():
            self.jobRunning = True
            ACTION.RUN(0)
            log = _translate('HandlerClass', 'Cycle started')
            if self.w.torch_enable.isChecked():
                log1 = _translate('HandlerClass', 'Torch enabled')
            else:
                log1 = _translate('HandlerClass', 'Torch disabled')
            STATUS.emit('update-machine-log', f'{log} - {log1}', 'TIME')
        self.refresh_button_states()

    def abort_pressed(self):
        if self.manualCut:
            ACTION.SET_SPINDLE_STOP(0)
            self.manualCut = False
            self.set_mc_states(True)
            if self.mcButton:
                self.button_normal(self.mcButton)
            log = _translate('HandlerClass', 'Manual cut aborted')
            STATUS.emit('update-machine-log', log, 'TIME')
            return
        elif self.probeTest:
            self.probe_test_stop()
            log = _translate('HandlerClass', 'Probe test aborted')
            STATUS.emit('update-machine-log', log, 'TIME')
            return
        elif self.torchPulse:
            self.torch_pulse_abort()
        elif self.framing:
            ACTION.ABORT()
            self.framing = False
            ACTION.SET_MANUAL_MODE()
            self.laserOnPin.set(0)
            self.w.gcodegraphics.logger.clear()
        else:
            ACTION.ABORT()
            if hal.get_value('plasmac.cut-recovery'):
                hal.set_p('plasmac.cut-recovery', '0')
                self.laserOnPin.set(0)
            self.interp_idle(None)
            log = _translate('HandlerClass', 'Cycle aborted')
            STATUS.emit('update-machine-log', log, 'TIME')

    def pause_resume_pressed(self):
        if hal.get_value('plasmac.cut-recovering'):
            self.w.jog_stack.setCurrentIndex(self.JOG)
            self.laserOnPin.set(0)
        if STATUS.is_auto_paused():
            log = _translate('HandlerClass', 'Cycle resumed')
            STATUS.emit('update-machine-log', log, 'TIME')

    def user_button_pressed(self, button):
        self.user_button_down(button)

    def user_button_released(self, button):
        self.user_button_up(button)

    def height_ovr_pressed(self, state, height):
        if state:
            if height:
                self.heightOvr += height * self.w.thc_threshold.value()
            else:
                self.heightOvr = 0
        if self.heightOvr < -9:
            self.heightOvr = -9
        if self.heightOvr > 9:
            self.heightOvr = 9
        self.heightOverridePin.set(self.heightOvr)
        self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}')

    def height_ovr_encoder(self, value):
        if (value != self.old_ovr_counts and self.extHeightOvrCountEnablePin.get()):
            self.heightOvr += (value-self.old_ovr_counts) * self.w.thc_threshold.value() * self.heightOvrScale
            if self.heightOvr < -9:
                self.heightOvr = -9
            if self.heightOvr > 9:
                self.heightOvr = 9
            self.heightOverridePin.set(self.heightOvr)
            self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}')
        self.old_ovr_counts = value

    def height_ovr_scale_change(self, value):
        if value:
            self.heightOvrScale = value

    def touch_xy_clicked(self):
        self.touch_off_xy(0, 0)
        self.w.gcodegraphics.logger.clear()

    def setup_feed_rate_changed(self):
        if self.w.probe_feed_rate.value() > self.w.setup_feed_rate.value():
            self.w.probe_feed_rate.setValue(self.w.setup_feed_rate.value())
        self.w.probe_feed_rate.setMaximum(self.w.setup_feed_rate.value())

    def save_plasma_clicked(self):
        self.save_plasma_parameters()

    def reload_plasma_clicked(self):
        self.load_plasma_parameters()

    def backup_clicked(self):
        self.save_logfile(6)
        bkpPath = f'{os.path.expanduser("~")}'
        bkpName = f'{self.machineName}_V{LCNCVER}-{VERSION}_{time.strftime("%y-%m-%d_%H-%M-%S")}.tar.gz'
        tmpFile = os.path.join(self.PATHS.CONFIGPATH, 'config_info.txt')
        lcncInfo = (Popen('linuxcnc_info -s', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8')
        network = (Popen('lspci | grep -i net', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8')
        with open(tmpFile, 'a') as outFile:
            outFile.write(f'locale:\n{os.getenv("LANG")}\n\n')
            if network:
                outFile.write(f'lspci | grep -i net:\n{network}\n')
            else:
                outFile.write('Unknown error with "lspci | grep -i net" command\n\n')
            if lcncInfo:
                outFile.write('linuxcnc_info:\n')
                try:
                    with open('/tmp/linuxcnc_info.txt', 'r') as inFile:
                        for line in inFile:
                            outFile.write(line)
                except:
                    outFile.write("Unknown error opening /tmp/linuxcnc_info.txt\n")
            else:
                outFile.write('Unknown error creating /tmp/linuxcnc_info.txt\n')
        with tarfile.open(f'{bkpPath}/{bkpName}', mode='w:gz', ) as archive:
            archive.add(f'{self.PATHS.CONFIGPATH}')
        try:
            os.remove(tmpFile)
        except:
            log = _translate('HandlerClass', 'Unknown error removing')
            STATUS.emit('update-machine-log', f'{log} {tmpFile}', 'TIME')
        head = _translate('HandlerClass', 'Backup Complete')
        msg0 = _translate('HandlerClass', 'A compressed backup of the machine configuration including the machine logs has been saved in your home directory as')
        msg1 = _translate('HandlerClass', 'It is safe to delete this file at any time')
        self.dialog_show_ok(QMessageBox.Information, head, f'\n{msg0}:\n{bkpName}\n\n{msg1}\n')

    def set_offsets_clicked(self):
        if self.developmentPin.get():
            reload(OFFSETS)
        self.w.main_tab_widget.setCurrentIndex(self.MAIN)
        if STATUS.stat.rotation_xy:
            ACTION.CALL_MDI_WAIT('G10 L2 P0 R0')
            ACTION.SET_MANUAL_MODE()
        OFFSETS.dialog_show(self, self.w, self.PREFS, INIPATH, STATUS, ACTION, TOOL)

    def feed_label_pressed(self):
        self.w.feed_slider.setValue(100)

    def rapid_label_pressed(self):
        self.w.rapid_slider.setValue(100)

    def jogs_label_pressed(self):
        self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL)

    def gui_button_jog(self, state, joint, direction):
        shift = False
        if state and joint == 'z' and direction == 1 and self.zPlusOverrideJog and self.w.chk_override_jog.isEnabled():
            self.w.chk_override_jog.setChecked(True)
        if STATUS.is_joint_mode():
            self.kb_jog(state, self.coordinates.index(joint), direction, shift)
        else:
            self.kb_jog(state, ['x', 'y', 'z', 'a', 'b', 'c'].index(joint), direction, shift)

    def view_t_pressed(self, widget):
        t = time.time() + 0.01
        while time.time() < t:
            QApplication.processEvents()
        widget.set_view('Z')
        mid = DRAW.extents_info(widget)[0]
        mult = 1 if self.units == 'in' else 25.4
        zoomScale = self.w.table_zoom_scale.value() * 2
        xTableCenter = (self.xMin + self.xLen / 2) / mult - mid[0]
        yTableCenter = (self.yMin + self.yLen / 2) / mult - mid[1]
        xSize = self.xLen / mult / zoomScale
        ySize = self.yLen / mult / zoomScale
        glTranslatef(-xTableCenter, -yTableCenter, 0)
        widget.set_eyepoint_from_extents(xSize, ySize)
        widget.perspective = False
        widget.lat = widget.lon = 0
        widget.update()

    def view_p_pressed(self):
        self.w.gcodegraphics.set_view('P')

    def view_z_pressed(self, widget):
        widget.set_view('Z')

    def view_clear_pressed(self):
        self.w.gcodegraphics.logger.clear()

    def pan_left_pressed(self, widget):
        widget.recordMouse(0, 0)
        widget.translateOrRotate(-widget._view_incr, 0)

    def pan_right_pressed(self, widget):
        widget.recordMouse(0, 0)
        widget.translateOrRotate(widget._view_incr, 0)

    def pan_up_pressed(self, widget):
        widget.recordMouse(0, 0)
        widget.translateOrRotate(0, -widget._view_incr)

    def pan_down_pressed(self, widget):
        widget.recordMouse(0, 0)
        widget.translateOrRotate(0, widget._view_incr)

    def zoom_in_pressed(self, widget):
        widget.zoomin()

    def zoom_out_pressed(self, widget):
        widget.zoomout()

    def gcode_display_loaded(self):
        gcodeLines = len(str(self.w.gcode_display.lines()))
        self.w.gcode_display.set_margin_metric(gcodeLines)
        self.w.gcode_editor.set_margin_metric(gcodeLines)

    def file_clear_clicked(self):
        proceed = self.editor_close_check()
        if not proceed:
            return
        self.w.preview_stack.setCurrentIndex(self.PREVIEW)
        self.w.gcode_editor.editor.new_text()
        self.w.gcode_editor.editor.setModified(False)
        # clear error message list and error status
        if self.w.screen_options.desktop_notify:
            self.w.screen_options.QTVCP_INSTANCE_._NOTICE.alarmpage = []
            self.w.screen_options.QTVCP_INSTANCE_._NOTICE.external_close()
        self.error_status(False)
        # return cut-type button to Normal
        if self.cutType:
            self.cutType = 0
            self.cutTypePin.set(0)
            self.button_normal(self.ctButton)
            self.w[self.ctButton].setText(self.cutTypeText)
        if self.fileOpened:
            if self.rflActive:
                self.clear_rfl()
            clearFile = f'{self.tmpPath}qtplasmac_file_clear.ngc'
            with open(clearFile, 'w') as outFile:
                outFile.write('m2')
            if ACTION.prefilter_path:
                if 'single_cut' in ACTION.prefilter_path:
                    self.preClearFile = self.oldFile
                else:
                    if self.lastLoadedProgram != 'None':
                        self.preClearFile = ACTION.prefilter_path or self.lastLoadedProgram
                    self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial))
                    self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex())
                    self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex())
            self.remove_temp_materials()
            ACTION.OPEN_PROGRAM(clearFile)
            ACTION.prefilter_path = self.preClearFile
            if self.tool != self.TORCH and STATUS.is_on_and_idle() and STATUS.is_all_homed():
                ACTION.CALL_MDI_WAIT('T0 M6')
                ACTION.CALL_MDI_WAIT('G43 H0')
                ACTION.SET_MANUAL_MODE()
            log = _translate('HandlerClass', 'Program cleared')
            STATUS.emit('update-machine-log', log, 'TIME')
        else:
            self.view_t_pressed(self.w.gcodegraphics)

    def file_open_clicked(self):
        if self.w.preview_stack.currentIndex() != self.OPEN:
            self.w.preview_stack.setCurrentIndex(self.OPEN)
        else:
            self.preview_index_return(self.w.preview_stack.currentIndex())

    def file_edit_clicked(self):
        if STATUS.stat.interp_state == linuxcnc.INTERP_IDLE and self.w.preview_stack.currentIndex() != self.EDIT:
            self.w.preview_stack.setCurrentIndex(self.EDIT)
            self.w.gcode_editor.editor.setFocus()
            if self.fileOpened and not self.w.gcode_editor.editor.isModified():
                self.w.gcode_editor.editor.load_text(ACTION.prefilter_path)
                text = _translate('HandlerClass', 'EDIT')
                self.w.edit_label.setText(f'{text}: {ACTION.prefilter_path}')
                self.w.gcode_editor.editor.setModified(False)
                try:
                    if os.path.getsize(self.gcodeErrorFile):
                        with open(self.gcodeErrorFile, 'r') as inFile:
                            self.w.gcode_editor.select_line(int(inFile.readline().rstrip()) - 1)
                            inFile.seek(0)
                            for line in inFile:
                                self.w.gcode_editor.editor.userHandle = \
                                    self.w.gcode_editor.editor.markerAdd(int(line) - 1, self.w.gcode_editor.editor.USER_MARKER_NUM)
                except:
                    pass
            self.vkb_show()
        else:
            self.new_exitCall(self.PREVIEW)

    def mdi_show_clicked(self):
        if STATUS.is_on_and_idle() and STATUS.is_all_homed() and self.w.gcode_stack.currentIndex() != self.MDI:
            self.w.gcode_stack.setCurrentIndex(self.MDI)
            self.vkb_show()
        else:
            self.w.gcode_stack.setCurrentIndex(self.GCODE)
            ACTION.SET_MANUAL_MODE()
            self.vkb_hide()

    def file_cancel_clicked(self):
        self.preview_index_return(self.w.preview_stack.currentIndex())

    def cone_size_changed(self, data):
        self.w.gcodegraphics.set_cone_basesize(data)

    def grid_size_changed(self, data):
        # grid size is in inches
        grid = data / self.unitsPerMm / 25.4
        self.w.gcodegraphics.grid_size = grid

    def main_tab_changed(self, tab):
        t = time.time() + 0.01
        while time.time() < t:
            QApplication.processEvents()
        if tab == self.MAIN:
            if self.w.preview_stack.currentIndex() == self.OPEN:
                self.vkb_show()
            else:
                self.vkb_hide()
            if self.w.gcode_stack.currentIndex() == self.GCODE and self.w.preview_stack.currentIndex() == self.PREVIEW:
                self.autorepeat_keys(False)
        elif tab == self.CONVERSATIONAL:
            self.w.conv_preview.logger.clear()
            self.w.conv_preview.set_current_view()
            if self.developmentPin.get():
                reload(self.CONV)
            self.CONV.conv_setup(self, self.w)
            self.vkb_show(True)
            self.autorepeat_keys(True)
        elif tab == self.PARAMETERS:
            self.vkb_show(True)
            self.autorepeat_keys(True)
        elif tab == self.SETTINGS:
            self.vkb_show()
            self.autorepeat_keys(True)
        elif tab == self.STATISTICS:
            self.vkb_hide()
            self.w.machinelog.scrollToBottom()
            self.w.machinelog.hideCursor()
            self.error_status(False)

    def z_height_changed(self, value):
        self.w.dro_z.update_user(value * self.droScale)

    def offsets_active_changed(self, value):
        if not value:
            # set z dro to normal made
            self.w.dro_z.setProperty('Qreference_type', 1)

    def consumable_change_changed(self, value):
        if self.ccButton:
            if value:
                log = _translate('HandlerClass', 'Consumable change initiated')
                STATUS.emit('update-machine-log', log, 'TIME')
            else:
                self.button_normal(self.ccButton)
                log = _translate('HandlerClass', 'Consumable change completed')
                STATUS.emit('update-machine-log', log, 'TIME')
        self.refresh_button_states()

    def plasmac_state_changed(self, state):
        if (state > self.PROBE_UP or state == self.PROBE_TEST) and hal.get_value('axis.z.eoffset-counts'):
            # set z dro to offset mode
            self.w.dro_z.setProperty('Qreference_type', 10)
        if state == self.IDLE:
            self.refresh_button_states()
        if state in [self.PROBE_HEIGHT, self.PROBE_DOWN, self.PROBE_UP,
                     self.ZERO_HEIGHT, self.PIERCE_HEIGHT, self.PUDDLE_JUMP,
                     self.CUT_HEIGHT, self.SAFE_HEIGHT, self.MAX_HEIGHT]:
            self.watchDogState = state
            self.watchDogTimer.start(3000)
        else:
            self.watchDogTimer.stop()
            self.watchDogState = None

    def plasmac_stop_changed(self, state):
        if not state and not self.plasmacStatePin.get():
            for pin in ['pierce-type', 'pierce-motion-delay', 'cut-height-delay', 'pierce-end-height',
                        'gouge-speed', 'gouge-speed-distance', 'creep-speed', 'creep-speed-distance']:
                hal.set_p(f'plasmac.{pin}', '0')

    def file_reload_clicked(self):
        proceed = self.editor_close_check()
        if not proceed:
            return
        if self.rflActive:
            self.clear_rfl()
        if ACTION.prefilter_path or self.lastLoadedProgram != 'None':
            file = ACTION.prefilter_path or self.lastLoadedProgram
            if os.path.exists(file):
                self.overlayProgress.setValue(0)
                self.remove_temp_materials()
                ACTION.OPEN_PROGRAM(file)
                log = _translate('HandlerClass', 'Reloaded')
                STATUS.emit('update-machine-log', f'{log}: {file}', 'TIME')
            else:
                head = _translate('HandlerClass', 'File Error')
                msg0 = _translate('HandlerClass', 'does not exist')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{file} {msg0}\n')

    def jog_slow_pressed(self, external=False):
        if self.w.jog_slow.isChecked():
            self.w.jog_slow.setText(_translate('HandlerClass', 'FAST'))
            self.w.jog_slider.setMaximum(self.w.jog_slider.maximum() * self.slowJogFactor)
            self.w.jog_slider.setValue(self.w.jog_slider.value() * self.slowJogFactor)
            self.w.jog_slider.setPageStep(100)
            self.previousJogSpeed = self.w.jog_slider.value()
            if external:
                self.w.jog_slow.setChecked(False)
        else:
            self.w.jog_slow.setText(_translate('HandlerClass', 'SLOW'))
            self.w.jog_slider.setValue(self.w.jog_slider.value() / self.slowJogFactor)
            self.w.jog_slider.setMaximum(self.w.jog_slider.maximum() / self.slowJogFactor)
            self.w.jog_slider.setPageStep(10)
            if external:
                self.w.jog_slow.setChecked(True)

    def chk_override_limits_changed(self, state):
        if state:
            ACTION.TOGGLE_LIMITS_OVERRIDE()

    def save_shutdown_message_clicked(self):
        self.PREFS.putpref('Exit warning text', self.w.sd_text.text(), str, 'GUI_OPTIONS')
        self.exitMessage = self.w.sd_text.text()

    def reload_shutdown_message_clicked(self):
        self.w.sd_text.setText(self.PREFS.getpref('Exit warning text', '', str, 'GUI_OPTIONS'))

    def save_user_button_clicked(self):
        self.set_interlock_defaults()
        for n in range(1, 21):
            self.PREFS.putpref(f'{n} Name', self.w[f'ub_name_{n}'].text(), str, 'BUTTONS')
            self.PREFS.putpref(f'{n} Code', self.w[f'ub_code_{n}'].text(), str, 'BUTTONS')
        self.interlockRules = INTERLOCK_RULES.copy()
        self.user_button_setup()
        self.preview_stack_changed()
        self.refresh_button_states()

    def reload_user_button_clicked(self):
        for n in range(1, 21):
            self.w[f'ub_name_{n}'].clear()
            self.w[f'ub_code_{n}'].clear()
        self.interlockRules = INTERLOCK_RULES.copy()
        self.user_button_setup()
        self.refresh_button_states()

    def web_back_pressed(self):
        self.w.webview.back()

    def web_forward_pressed(self):
        self.w.webview.forward()

    def web_reload_pressed(self):
        self.w.webview.load(self.umUrl)

#########################################################################################################################
# GENERAL FUNCTIONS #
#########################################################################################################################

    # called by ScreenOptions, this function overrides ScreenOption's closeEvent
    def closeEvent(self, event):
        O = self.w.screen_options
        if self.w.chk_exit_warning.isChecked() or not STATUS.is_interp_idle():
            icon = QMessageBox.Question if STATUS.is_interp_idle() else QMessageBox.Critical
            head = _translate('HandlerClass', 'Shutdown')
            if not STATUS.is_interp_idle():
                msg0 = _translate('HandlerClass', 'Current operation is not complete')
                msg0 += '!\n\n'
            else:
                msg0 = ''
            if self.exitMessage:
                exitLines = self.exitMessage.split('\\')
                for line in exitLines:
                    msg0 += f'{line}\n'
                msg0 += '\n'
            msg0 += _translate('HandlerClass', 'Do you want to shutdown QtPlasmaC')
            if self.dialog_show_yesno(icon, head, f'\n{msg0}?\n'):
                if O.PREFS_ and O.play_sounds and O.shutdown_play_sound:
                    STATUS.emit('play-sound', O.shutdown_exit_sound_type)
                O.QTVCP_INSTANCE_.settings.sync()
                O.QTVCP_INSTANCE_.shutdown()
                O.QTVCP_INSTANCE_.panel_.shutdown()
                STATUS.shutdown()
                event.accept()
            else:
                event.ignore()
        else:
            if O.PREFS_ and O.play_sounds and O.shutdown_play_sound:
                STATUS.emit('play-sound', O.shutdown_exit_sound_type)
            O.QTVCP_INSTANCE_.settings.sync()
            O.QTVCP_INSTANCE_.shutdown()
            O.QTVCP_INSTANCE_.panel_.shutdown()
            STATUS.shutdown()

    def update_check(self):
        # newest update must be added last in this function
        # if any writing to the INI file is required then that needs
        # to be done later in the update_iniwrite function
        halfiles = self.iniFile.findall('HAL', 'HALFILE') or None
        qtvcpPrefsFile = os.path.join(self.PATHS.CONFIGPATH, 'qtvcp.prefs')
        self.restart = False
        # use qtplasmac_comp.hal for component connections (pre V1.221.154 2022/01/18)
        if halfiles and not [f for f in halfiles if 'plasmac.tcl' in f] and not \
                [f for f in halfiles if 'qtplasmac_comp.hal' in f]:
            restart, error, text = UPDATER.add_component_hal_file(self.PATHS.CONFIGPATH, halfiles)
            self.updateData.append([restart, error, text])
            if error:
                return
            self.updateIni[154] = True
        # split out qtplasmac specific prefs into a separate file (pre V1.222.170 2022/03/08)
        if not os.path.isfile(self.prefsFile):
            old = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac.prefs')
            if os.path.isfile(old):
                restart, error, text = UPDATER.split_prefs_file(old, qtvcpPrefsFile, self.prefsFile)
                self.updateData.append([restart, error, text])
                if error:
                    return
            self.PREFS = Access(self.prefsFile)
        # move conversational prefs from qtvcp.prefs to <machine_name>.prefs (pre V1.222.187 2022/05/03)
        if os.path.isfile(qtvcpPrefsFile) and os.path.isfile(self.prefsFile):
            with open(qtvcpPrefsFile, 'r') as inFile:
                data = inFile.readlines()
                if [line for line in data if '[CONVERSATIONAL]' in line]:
                    restart, error, text = UPDATER.move_prefs(qtvcpPrefsFile, self.prefsFile)
                    self.updateData.append([restart, error, text])
                    if error:
                        return
        # change RS274 startup parameters from a subroutine (pre V1.224.207 2022/06/22)
        startupCode = self.iniFile.find('RS274NGC', 'RS274NGC_STARTUP_CODE')
        if 'metric_startup' in startupCode or 'imperial_startup' in startupCode:
            self.updateIni[207] = True
        # remove the qtplasmac link from the config directory (pre V1.225.208 2022/06/29)
        if os.path.islink(os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac')):
            # stage 1: set up for unlinking on the next run of qtplasmac
            if 'code.py' in self.iniFile.find('FILTER', 'ngc'):
                self.updateIni[208] = True
            # stage 2: remove the qtplasmac link
            else:
                os.unlink(os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac'))
        # move qtplasmac options from INI file to prefs file pre V1.227.219 2022/07/14)
        if not self.PREFS.has_section('BUTTONS'):
            update = False
            for n in range(1, 21):
                if self.iniFile.find('QTPLASMAC', f'BUTTON_{n}_NAME'):
                    update = True
                    break
            if update:
                restart, error, text = UPDATER.move_options_to_prefs_file(self.iniFile, self.PREFS)
                self.updateData.append([restart, error, text])
                if error:
                    return
                self.updateIni[219] = True
        # move port info from [GUI_OPTIONS] section (if it was moved via V1.227.219 update) to [POWERMAX] section
        if self.PREFS.has_option('GUI_OPTIONS', 'Port'):
            restart, error, text = UPDATER.move_port(self.PREFS)
            self.updateData.append([restart, error, text])
            if error:
                return
        # move default material from prefs file to material 0 in materials file (pre V1.236.278 2023/07/07)
        if self.PREFS.has_section('DEFAULT MATERIAL'):
            restart, error, text = UPDATER.move_default_material(self.PREFS, self.materialFile, self.unitsPerMm)
            self.updateData.append([restart, error, text])
            if error:
                return
        # change runcritical to cutcritical in <machine_name>.prefs file (pre V2.10-001.015 2023/12/23)
        if os.path.isfile(self.prefsFile):
            with open(self.prefsFile, 'r') as inFile:
                text = inFile.read()
                if 'runcritical' in text:
                    restart, error, text = UPDATER.rename_runcritical(self.prefsFile)
                    self.updateData.append([restart, error, text])
                    if error:
                        return
        # set user_m_path to include the nc_files directory (pre V2.10-001.017 2024/01/23)
        mPathIni = self.iniFile.find('RS274NGC', 'USER_M_PATH')
        if 'nc_files/plasmac/m_files' not in mPathIni:
            if '/usr' in self.PATHS.BASEDIR:
                mPath = '/usr/share/doc/linuxcnc/examples/nc_files/plasmac/m_files'
                # simPath = os.path.join(self.PATHS.BASEDIR, 'share/doc/linuxcnc/examples/sample-configs/sim/qtplasmac')
                # we need elevated privileges to remove a file from here so forget it...
                # we may revisit this.
                simPath = None
            else:
                mPath = os.path.realpath(os.path.join(self.PATHS.BASEDIR, 'nc_files/plasmac/m_files'))
                simPath = os.path.join(self.PATHS.BASEDIR, 'configs/sim/qtplasmac')
            restart, error, text = UPDATER.insert_user_m_path(self.PATHS.CONFIGPATH, simPath)
            if error:
                return
            self.updateIni['001-017'] = mPath
        # add M52P1 to the [CONVERSATIONAL] Preamble and Postamble (pre V2.10-009.070 2025/08/07)
        if any(self.PREFS.has_option('CONVERSATIONAL', option) and 'M52P1' not in self.PREFS.get('CONVERSATIONAL', option) for option in ('Preamble', 'Postamble')):
            restart, error, text = UPDATER.add_adaptive(self.PREFS)
            self.updateData.append([restart, error, text])
            if error:
                return
        # add newlines between codes [CONVERSATIONAL] Preamble and Postamble (pre V2.10-009.071 2025/08/09)
        if any(self.PREFS.has_option('CONVERSATIONAL', option) and ' ' in self.PREFS.get('CONVERSATIONAL', option) for option in ('Preamble', 'Postamble')):
            restart, error, text = UPDATER.add_newlines(self.PREFS)
            self.updateData.append([restart, error, text])
            if error:
                return

    def update_iniwrite(self):
        # this is for updates that write to the INI file
        if 154 in self.updateIni:
            restart, error, text = UPDATER.add_component_hal_file_iniwrite(INIPATH)
            if restart:
                self.restart = True
            self.updateData.append([self.restart, error, text])
            if error:
                return
        if 207 in self.updateIni:
            restart, error, text = UPDATER.rs274ngc_startup_code_iniwrite(INIPATH)
            if restart:
                self.restart = True
            self.updateData.append([self.restart, error, text])
            if error:
                return
        if 208 in self.updateIni:
            restart, error, text = UPDATER.remove_qtplasmac_link_iniwrite(INIPATH)
            if restart:
                self.restart = True
            self.updateData.append([self.restart, error, text])
            if error:
                return
        if 219 in self.updateIni:
            restart, error, text = UPDATER.move_options_to_prefs_file_iniwrite(INIPATH)
            if restart:
                self.restart = True
            self.updateData.append([self.restart, error, text])
            if error:
                return
        if '001-017' in self.updateIni:
            restart, error, text = UPDATER.insert_user_m_path_iniwrite(INIPATH, self.updateIni['001-017'])
            if restart:
                self.restart = True
            self.updateData.append([self.restart, error, text])
            if error:
                return

    def find_a_file(self, name, path):
        ''' find a file "name" in the path "path" '''
        for root, dirs, files in os.walk(path):
            # variable is not currently used
            del dirs
            if name in files:
                return os.path.join(root, name)

    def motion_type_changed(self, value):
        if value == 0 and STATUS.is_mdi_mode():
            ACTION.SET_MANUAL_MODE()

    def set_interlock_defaults(self):
        self.ccButton, self.otButton, self.ptButton, self.tpButton = '', '', '', ''
        self.ctButton, self.scButton, self.frButton, self.mcButton = '', '', '', ''
        self.ovButton, self.llButton, self.tlButton, self.umButton, self.jtButton = '', '', [], '', ''
        self.halTogglePins = {}
        self.halPulsePins = {}
        self.dualCodeButtons = {}

    def preview_index_return(self, index):
        if self.w.gcode_editor.editor.isModified():
            self.new_exitCall(index)
        else:
            if index == self.PREVIEW:
                pass
            elif index == self.OPEN:
                self.vkb_hide()
                ACTION.SET_MANUAL_MODE()
            elif index == self.EDIT:
                pass
            elif index == self.CAMERA:
                self.button_normal('camera')
                self.cameraOn = False
                self.vkb_hide()
                ACTION.SET_MANUAL_MODE()
            elif index == self.OFFSETS:
                pass
            elif index == self.USER_MANUAL:
                pass
            self.w.preview_stack.setCurrentIndex(self.PREVIEW)

    def editor_close_check(self):
        if self.w.gcode_editor.editor.isModified():
            head = _translate('HandlerClass', 'Unsaved Editor Changes')
            msg0 = _translate('HandlerClass', 'Unsaved changes will be lost')
            msg1 = _translate('HandlerClass', 'Do you want to proceed')
            if not self.dialog_show_yesno(QMessageBox.Question, head, f'\n{msg0}\n\n{msg1}?\n'):
                self.w.preview_stack.setCurrentIndex(self.EDIT)
                self.w.gcode_editor.editor.setFocus()
                return False
        return True

    def system_notify_button_pressed(self, object, button, state):
        if button in ['background', 'clearAll', 'close', 'lastFive'] and state:
            self.error_status(False)

    def error_status(self, state):
        self.error_present = state
        if state:
            text = _translate('HandlerClass', 'ERROR SENT TO MACHINE LOG')
        else:
            text = ''
        self.w.error_label.setText(f'{text}')

    def touch_off_xy(self, x, y):
        if STATUS.is_on_and_idle() and STATUS.is_all_homed():
            ACTION.CALL_MDI_WAIT(f'G10 L20 P0 X{x} Y{y}')
            if self.fileOpened:
                self.file_reload_clicked()
            ACTION.SET_MANUAL_MODE()

    def bounds_check_file(self):
        msg = _translate('HandlerClass', 'G-code')
        self.boundsMultiplier = 1
        if 'gcode_units' in self.gcodeProps:
            if self.units == 'in' and self.gcodeProps['gcode_units'] == 'mm':
                self.boundsMultiplier = 0.03937
            elif self.units == 'mm' and self.gcodeProps['gcode_units'] == 'in':
                self.boundsMultiplier = 25.4
        xMin = float(self.gcodeProps['x'].split()[0]) * self.boundsMultiplier
        xMax = float(self.gcodeProps['x'].split()[2]) * self.boundsMultiplier
        yMin = float(self.gcodeProps['y'].split()[0]) * self.boundsMultiplier
        yMax = float(self.gcodeProps['y'].split()[2]) * self.boundsMultiplier
        errMsg = self.bounds_compare(xMin, xMax, yMin, yMax, msg)
        return (True, errMsg) if errMsg else (False, '')

    def bounds_check_probe(self, static):
        if not (self.probeOffsetX or self.probeOffsetY) and not self.cutTypePin.get():
            return False, ''
        msg = _translate('HandlerClass', 'Move to probe coordinate')
        xPierceOffset = yPierceOffset = xMinP = xMaxP = yMinP = yMaxP = 0
        if static:
            xMinP = xMaxP = STATUS.get_position()[0][0] + self.probeOffsetX
            yMinP = yMaxP = STATUS.get_position()[0][1] + self.probeOffsetY
        elif any((self.xMinPierceExtentPin.get(), self.xMaxPierceExtentPin.get(),
                  self.yMinPierceExtentPin.get(), self.yMaxPierceExtentPin.get())):
            if self.cutTypePin.get():
                xPierceOffset = self.w.x_pierce_offset.value()
                yPierceOffset = self.w.y_pierce_offset.value()
            if self.probeOffsetX or (xPierceOffset and self.cutTypePin.get()):
                xMinP = self.xMinPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetX + STATUS.stat.g5x_offset[0] + xPierceOffset
                xMaxP = self.xMaxPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetX + STATUS.stat.g5x_offset[0] + xPierceOffset
            if self.probeOffsetY or (yPierceOffset and self.cutTypePin.get()):
                yMinP = self.yMinPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetY + STATUS.stat.g5x_offset[1] + yPierceOffset
                yMaxP = self.yMaxPierceExtentPin.get() * self.boundsMultiplier + self.probeOffsetY + STATUS.stat.g5x_offset[1] + yPierceOffset
        errMsg = self.bounds_compare(xMinP, xMaxP, yMinP, yMaxP, msg)
        return (True, errMsg) if errMsg else (False, '')

    def bounds_check_framing(self, xOffset=0, yOffset=0, laser=False):
        msg = _translate('HandlerClass', 'Framing move')
        msg1 = ''
        if laser:
            msg1 = _translate('HandlerClass', 'due to laser offset')
        xStart = STATUS.stat.g5x_offset[0] + xOffset
        yStart = STATUS.stat.g5x_offset[1] + yOffset
        xMin = float(self.gcodeProps['x_zero_rxy'].split()[0]) * self.boundsMultiplier + xOffset
        xMax = float(self.gcodeProps['x_zero_rxy'].split()[2]) * self.boundsMultiplier + xOffset
        yMin = float(self.gcodeProps['y_zero_rxy'].split()[0]) * self.boundsMultiplier + yOffset
        yMax = float(self.gcodeProps['y_zero_rxy'].split()[2]) * self.boundsMultiplier + yOffset
        coordinates = [[xStart, yStart], [xMin, yMin], [xMin, yMax], [xMax, yMax], [xMax, yMin]]
        framePoints, xMin, yMin, xMax, yMax = self.rotate_frame(coordinates)
        errMsg = self.bounds_compare(xMin, xMax, yMin, yMax, msg, msg1)
        return (errMsg, framePoints)

    def bounds_compare(self, xMin, xMax, yMin, yMax, msg, msg1=''):
        errMsg = ''
        epsilon = 1e-4
        txtxMin = _translate('HandlerClass', 'exceeds the X minimum limit by')
        txtxMax = _translate('HandlerClass', 'exceeds the X maximum limit by')
        txtyMin = _translate('HandlerClass', 'exceeds the Y minimum limit by')
        txtyMax = _translate('HandlerClass', 'exceeds the Y maximum limit by')
        lessThan = _translate('HandlerClass', 'less than')
        if xMin < self.xMin:
            errMsg += f'{msg} {txtxMin} {lessThan} {epsilon} {self.units} {msg1}\n' if (self.xMin - xMin) < epsilon \
                else f'{msg} {txtxMin} {self.xMin - xMin:0.4f} {self.units} {msg1}\n'
        if xMax > self.xMax:
            errMsg += f'{msg} {txtxMax} {lessThan} {epsilon} {self.units} {msg1}\n' if (xMax - self.xMax) < epsilon \
                else f'{msg} {txtxMax} {xMax - self.xMax:0.4f} {self.units} {msg1}\n'
        if yMin < self.yMin:
            errMsg += f'{msg} {txtyMin} {lessThan} {epsilon} {self.units} {msg1}\n' if (self.yMin - yMin) < epsilon \
                else f'{msg} {txtyMin} {self.yMin - yMin:0.4f} {self.units} {msg1}\n'
        if yMax > self.yMax:
            errMsg += f'{msg} {txtyMax} {lessThan} {epsilon} {self.units} {msg1}\n' if (yMax - self.yMax) < epsilon \
                else f'{msg} {txtyMax} {yMax - self.yMax:0.4f} {self.units} {msg1}\n'
        return errMsg

    def rotate_frame(self, coordinates):
        angle = math.radians(STATUS.stat.rotation_xy)
        cos = math.cos(angle)
        sin = math.sin(angle)
        framePoints = [coordinates[0]]
        ox = framePoints[0][0]
        oy = framePoints[0][1]
        for x, y in coordinates[1:]:
            tox = x - ox
            toy = y - oy
            rx = (tox * cos) - (toy * sin) + ox
            ry = (tox * sin) + (toy * cos) + oy
            framePoints.append([rx, ry])
        xMin = min(framePoints[1:])[0]
        xMax = max(framePoints[1:])[0]
        yMin = min(framePoints[1:])[1]
        yMax = max(framePoints[1:])[1]
        return framePoints, xMin, yMin, xMax, yMax

    def save_plasma_parameters(self):
        self.PREFS.putpref('Arc OK High', self.w.arc_ok_high.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc OK Low', self.w.arc_ok_low.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc Maximum Starts', self.w.arc_max_starts.value(), int, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc Fail Timeout', self.w.arc_fail_delay.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc Voltage Offset', self.w.arc_voltage_offset.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc Voltage Scale', self.w.arc_voltage_scale.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Velocity Anti Dive Threshold', self.w.cornerlock_threshold.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Float Switch Travel', self.w.float_switch_travel.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Height Per Volt', self.w.height_per_volt.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Void Sense Slope', self.w.voidlock_slope.value(), int, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Offset Feed Rate', self.w.offset_feed_rate.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Ohmic Maximum Attempts', self.w.ohmic_max_attempts.value(), int, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Ohmic Probe Offset', self.w.ohmic_probe_offset.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Pid P Gain', self.w.pid_p_gain.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Pid D Gain', self.w.pid_d_gain.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Pid I Gain', self.w.pid_i_gain.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Probe Feed Rate', self.w.probe_feed_rate.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Probe Start Height', self.w.probe_start_height.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Slat Height', self.w.slat_height.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Arc Restart Delay', self.w.arc_restart_delay.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Safe Height', self.w.safe_height.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Scribe Arming Delay', self.w.scribe_arm_delay.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Scribe On Delay', self.w.scribe_on_delay.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Setup Feed Rate', self.w.setup_feed_rate.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Skip IHS Distance', self.w.skip_ihs_distance.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Spotting Threshold', self.w.spotting_threshold.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Spotting Time', self.w.spotting_time.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('THC Delay', self.w.thc_delay.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('THC Sample Counts', self.w.thc_sample_counts.value(), int, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('THC Sample Threshold', self.w.thc_sample_threshold.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('THC Threshold', self.w.thc_threshold.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('X Pierce Offset', self.w.x_pierce_offset.value(), float, 'PLASMA_PARAMETERS')
        self.PREFS.putpref('Y Pierce Offset', self.w.y_pierce_offset.value(), float, 'PLASMA_PARAMETERS')

    def load_plasma_parameters(self):
        self.w.arc_fail_delay.setValue(self.PREFS.getpref('Arc Fail Timeout', 3.0, float, 'PLASMA_PARAMETERS'))
        self.w.arc_ok_high.setValue(self.PREFS.getpref('Arc OK High', 250.0, float, 'PLASMA_PARAMETERS'))
        self.w.arc_ok_low.setValue(self.PREFS.getpref('Arc OK Low', 60.0, float, 'PLASMA_PARAMETERS'))
        self.w.arc_max_starts.setValue(self.PREFS.getpref('Arc Maximum Starts', 3, int, 'PLASMA_PARAMETERS'))
        self.w.arc_voltage_offset.setValue(self.PREFS.getpref('Arc Voltage Offset', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.arc_voltage_scale.setValue(self.PREFS.getpref('Arc Voltage Scale', 1.0, float, 'PLASMA_PARAMETERS'))
        self.w.cornerlock_threshold.setValue(self.PREFS.getpref('Velocity Anti Dive Threshold', 90.0, float, 'PLASMA_PARAMETERS'))
        self.w.float_switch_travel.setValue(self.PREFS.getpref('Float Switch Travel', round(1.5 * self.unitsPerMm, 2), float, 'PLASMA_PARAMETERS'))
        self.w.height_per_volt.setValue(self.PREFS.getpref('Height Per Volt', round(0.1 * self.unitsPerMm, 3), float, 'PLASMA_PARAMETERS'))
        self.w.offset_feed_rate.setValue(self.PREFS.getpref('Offset Feed Rate', self.offsetFeedRate * 0.8, float, 'PLASMA_PARAMETERS'))
        self.w.ohmic_max_attempts.setValue(self.PREFS.getpref('Ohmic Maximum Attempts', 0, int, 'PLASMA_PARAMETERS'))
        self.w.ohmic_probe_offset.setValue(self.PREFS.getpref('Ohmic Probe Offset', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.pid_p_gain.setValue(self.PREFS.getpref('Pid P Gain', 10.0, float, 'PLASMA_PARAMETERS'))
        self.w.pid_d_gain.setValue(self.PREFS.getpref('Pid D Gain', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.pid_i_gain.setValue(self.PREFS.getpref('Pid I Gain', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.probe_feed_rate.setValue(self.PREFS.getpref('Probe Feed Rate', round(300.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS'))
        self.w.probe_start_height.setValue(self.PREFS.getpref('Probe Start Height', round(25.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS'))
        self.w.slat_height.setValue(self.PREFS.getpref('Slat Height', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.arc_restart_delay.setValue(self.PREFS.getpref('Arc Restart Delay', 1.0, float, 'PLASMA_PARAMETERS'))
        self.w.safe_height.setValue(self.PREFS.getpref('Safe Height', round(25.0 * self.unitsPerMm, 0), float, 'PLASMA_PARAMETERS'))
        self.w.setup_feed_rate.setValue(self.PREFS.getpref('Setup Feed Rate', self.thcFeedRate * 0.8, float, 'PLASMA_PARAMETERS'))
        self.w.scribe_arm_delay.setValue(self.PREFS.getpref('Scribe Arming Delay', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.scribe_on_delay.setValue(self.PREFS.getpref('Scribe On Delay', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.skip_ihs_distance.setValue(self.PREFS.getpref('Skip IHS Distance', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.spotting_threshold.setValue(self.PREFS.getpref('Spotting Threshold', 1.0, float, 'PLASMA_PARAMETERS'))
        self.w.spotting_time.setValue(self.PREFS.getpref('Spotting Time', 0.0, float, 'PLASMA_PARAMETERS'))
        self.w.thc_delay.setValue(self.PREFS.getpref('THC Delay', 0.5, float, 'PLASMA_PARAMETERS'))
        self.w.thc_sample_counts.setValue(self.PREFS.getpref('THC Sample Counts', 50, int, 'PLASMA_PARAMETERS'))
        self.w.thc_sample_threshold.setValue(self.PREFS.getpref('THC Sample Threshold', 1.0, float, 'PLASMA_PARAMETERS'))
        self.w.thc_threshold.setValue(self.PREFS.getpref('THC Threshold', 1.0, float, 'PLASMA_PARAMETERS'))
        self.w.voidlock_slope.setValue(self.PREFS.getpref('Void Sense Slope', 500, int, 'PLASMA_PARAMETERS'))
        self.w.x_pierce_offset.setValue(self.PREFS.getpref('X Pierce Offset', round(1.6 * self.unitsPerMm, 2), float, 'PLASMA_PARAMETERS'))
        self.w.y_pierce_offset.setValue(self.PREFS.getpref('Y Pierce Offset', 0.0, float, 'PLASMA_PARAMETERS'))

    def set_signal_connections(self):
        self.w.power.pressed.connect(lambda: self.power_button("pressed", True))
        self.w.power.released.connect(lambda: self.power_button("released", False))
        self.w.power.clicked.connect(lambda: self.power_button("clicked", None))
        self.w.run.clicked.connect(self.run_clicked)
        self.w.pause_resume.pressed.connect(self.pause_resume_pressed)
        self.w.abort.pressed.connect(self.abort_pressed)
        self.w.file_reload.clicked.connect(self.file_reload_clicked)
        self.w.jog_slow.pressed.connect(self.jog_slow_pressed)
        self.w.chk_soft_keyboard.stateChanged.connect(self.soft_keyboard)
        self.w.chk_override_limits.stateChanged.connect(self.chk_override_limits_changed)
        self.w.chk_overlay.stateChanged.connect(self.overlay_update)
        self.w.chk_tool_tips.stateChanged.connect(lambda: TOOLTIPS.tool_tips_changed(self, self.w))
        self.w.torch_enable.stateChanged.connect(lambda w: self.torch_enable_changed(w))
        self.w.ohmic_probe_enable.stateChanged.connect(self.refresh_button_states)
        self.w.thc_auto.stateChanged.connect(lambda w: self.thc_auto_changed(w))
        self.w.cone_size.valueChanged.connect(self.cone_size_changed)
        self.w.grid_size.valueChanged.connect(self.grid_size_changed)
        self.w.gcode_display.linesChanged.connect(self.gcode_display_loaded)
        self.w.gcode_editor.percentDone.connect(lambda w: self.progress_changed(None, w))
        self.w.file_clear.clicked.connect(self.file_clear_clicked)
        self.w.file_open.clicked.connect(self.file_open_clicked)
        self.w.file_edit.clicked.connect(self.file_edit_clicked)
        self.w.mdi_show.clicked.connect(self.mdi_show_clicked)
        self.w.file_cancel.clicked.connect(self.file_cancel_clicked)
        self.w.color_foregrnd.clicked.connect(lambda: self.openColorDialog(self.w.color_foregrnd))
        self.w.color_foregalt.clicked.connect(lambda: self.openColorDialog(self.w.color_foregalt))
        self.w.color_led.clicked.connect(lambda: self.openColorDialog(self.w.color_led))
        self.w.color_backgrnd.clicked.connect(lambda: self.openColorDialog(self.w.color_backgrnd))
        self.w.color_backgalt.clicked.connect(lambda: self.openColorDialog(self.w.color_backgalt))
        self.w.color_frams.clicked.connect(lambda: self.openColorDialog(self.w.color_frams))
        self.w.color_estop.clicked.connect(lambda: self.openColorDialog(self.w.color_estop))
        self.w.color_disabled.clicked.connect(lambda: self.openColorDialog(self.w.color_disabled))
        self.w.color_preview.clicked.connect(lambda: self.openColorDialog(self.w.color_preview))
        self.w.save_plasma.clicked.connect(self.save_plasma_clicked)
        self.w.reload_plasma.clicked.connect(self.reload_plasma_clicked)
        self.w.backup.clicked.connect(self.backup_clicked)
        self.w.set_offsets.clicked.connect(self.set_offsets_clicked)
        self.w.save_material.clicked.connect(self.save_materials_clicked)
        self.w.reload_material.clicked.connect(self.reload_materials_clicked)
        self.w.new_material.clicked.connect(lambda: self.new_material_clicked(0, 0))
        self.w.delete_material.clicked.connect(self.delete_material_clicked)
        self.w.setup_feed_rate.valueChanged.connect(self.setup_feed_rate_changed)
        self.w.touch_xy.clicked.connect(self.touch_xy_clicked)
        self.w.materials_box.currentIndexChanged.connect(lambda w: self.material_changed(w))
        self.w.material_selector.currentIndexChanged.connect(lambda w: self.selector_changed(w))
        self.w.conv_material.currentIndexChanged.connect(lambda w: self.conv_material_changed(w))
        self.w.default_material.currentIndexChanged.connect(lambda w: self.default_material_changed(w))
        self.w.sd_save.clicked.connect(self.save_shutdown_message_clicked)
        self.w.sd_reload.clicked.connect(self.reload_shutdown_message_clicked)
        self.w.ub_save.clicked.connect(self.save_user_button_clicked)
        self.w.ub_reload.clicked.connect(self.reload_user_button_clicked)
        self.materialChangePin.value_changed.connect(lambda w: self.material_change_pin_changed(w))
        self.materialChangeNumberPin.value_changed.connect(lambda w: self.material_change_number_pin_changed(w))
        self.materialChangeTimeoutPin.value_changed.connect(lambda w: self.material_change_timeout_pin_changed(w))
        self.materialReloadPin.value_changed.connect(lambda w: self.material_reload_pin_changed(w))
        self.materialTempPin.value_changed.connect(lambda w: self.material_temp_pin_changed(w))
        self.w.height_lower.pressed.connect(lambda: self.height_ovr_pressed(1, -1))
        self.w.height_raise.pressed.connect(lambda: self.height_ovr_pressed(1, 1))
        self.w.height_reset.pressed.connect(lambda: self.height_ovr_pressed(1, 0))
        self.w.button_1.pressed.connect(lambda: self.user_button_pressed(1))
        self.w.button_1.released.connect(lambda: self.user_button_released(1))
        self.w.button_2.pressed.connect(lambda: self.user_button_pressed(2))
        self.w.button_2.released.connect(lambda: self.user_button_released(2))
        self.w.button_3.pressed.connect(lambda: self.user_button_pressed(3))
        self.w.button_3.released.connect(lambda: self.user_button_released(3))
        self.w.button_4.pressed.connect(lambda: self.user_button_pressed(4))
        self.w.button_4.released.connect(lambda: self.user_button_released(4))
        self.w.button_5.pressed.connect(lambda: self.user_button_pressed(5))
        self.w.button_5.released.connect(lambda: self.user_button_released(5))
        self.w.button_6.pressed.connect(lambda: self.user_button_pressed(6))
        self.w.button_6.released.connect(lambda: self.user_button_released(6))
        self.w.button_7.pressed.connect(lambda: self.user_button_pressed(7))
        self.w.button_7.released.connect(lambda: self.user_button_released(7))
        self.w.button_8.pressed.connect(lambda: self.user_button_pressed(8))
        self.w.button_8.released.connect(lambda: self.user_button_released(8))
        self.w.button_9.pressed.connect(lambda: self.user_button_pressed(9))
        self.w.button_9.released.connect(lambda: self.user_button_released(9))
        self.w.button_10.pressed.connect(lambda: self.user_button_pressed(10))
        self.w.button_10.released.connect(lambda: self.user_button_released(10))
        self.w.button_11.pressed.connect(lambda: self.user_button_pressed(11))
        self.w.button_11.released.connect(lambda: self.user_button_released(11))
        self.w.button_12.pressed.connect(lambda: self.user_button_pressed(12))
        self.w.button_12.released.connect(lambda: self.user_button_released(12))
        self.w.button_13.pressed.connect(lambda: self.user_button_pressed(13))
        self.w.button_13.released.connect(lambda: self.user_button_released(13))
        self.w.button_14.pressed.connect(lambda: self.user_button_pressed(14))
        self.w.button_14.released.connect(lambda: self.user_button_released(14))
        self.w.button_15.pressed.connect(lambda: self.user_button_pressed(15))
        self.w.button_15.released.connect(lambda: self.user_button_released(15))
        self.w.button_16.pressed.connect(lambda: self.user_button_pressed(16))
        self.w.button_16.released.connect(lambda: self.user_button_released(16))
        self.w.button_17.pressed.connect(lambda: self.user_button_pressed(17))
        self.w.button_17.released.connect(lambda: self.user_button_released(17))
        self.w.button_18.pressed.connect(lambda: self.user_button_pressed(18))
        self.w.button_18.released.connect(lambda: self.user_button_released(18))
        self.w.button_19.pressed.connect(lambda: self.user_button_pressed(19))
        self.w.button_19.released.connect(lambda: self.user_button_released(19))
        self.w.button_20.pressed.connect(lambda: self.user_button_pressed(20))
        self.w.button_20.released.connect(lambda: self.user_button_released(20))
        self.w.cut_rec_speed.valueChanged.connect(lambda w: self.cutrec_speed_changed(w))
        self.w.kerf_width.valueChanged.connect(lambda w: self.cutrec_move_changed(w))
        self.w.jog_x_plus.pressed.connect(lambda: self.gui_button_jog(1, 'x', 1))
        self.w.jog_x_plus.released.connect(lambda: self.gui_button_jog(0, 'x', 1))
        self.w.jog_x_minus.pressed.connect(lambda: self.gui_button_jog(1, 'x', -1))
        self.w.jog_x_minus.released.connect(lambda: self.gui_button_jog(0, 'x', -1))
        self.w.jog_y_plus.pressed.connect(lambda: self.gui_button_jog(1, 'y', 1))
        self.w.jog_y_plus.released.connect(lambda: self.gui_button_jog(0, 'y', 1))
        self.w.jog_y_minus.pressed.connect(lambda: self.gui_button_jog(1, 'y', -1))
        self.w.jog_y_minus.released.connect(lambda: self.gui_button_jog(0, 'y', -1))
        self.w.jog_z_plus.pressed.connect(lambda: self.gui_button_jog(1, 'z', 1))
        self.w.jog_z_plus.released.connect(lambda: self.gui_button_jog(0, 'z', 1))
        self.w.jog_z_minus.pressed.connect(lambda: self.gui_button_jog(1, 'z', -1))
        self.w.jog_z_minus.released.connect(lambda: self.gui_button_jog(0, 'z', -1))
        self.w.jog_a_plus.pressed.connect(lambda: self.gui_button_jog(1, 'a', 1))
        self.w.jog_a_plus.released.connect(lambda: self.gui_button_jog(0, 'a', 1))
        self.w.jog_a_minus.pressed.connect(lambda: self.gui_button_jog(1, 'a', -1))
        self.w.jog_a_minus.released.connect(lambda: self.gui_button_jog(0, 'a', -1))
        self.w.jog_b_plus.pressed.connect(lambda: self.gui_button_jog(1, 'b', 1))
        self.w.jog_b_plus.released.connect(lambda: self.gui_button_jog(0, 'b', 1))
        self.w.jog_b_minus.pressed.connect(lambda: self.gui_button_jog(1, 'b', -1))
        self.w.jog_b_minus.released.connect(lambda: self.gui_button_jog(0, 'b', -1))
        self.w.jog_c_plus.pressed.connect(lambda: self.gui_button_jog(1, 'c', 1))
        self.w.jog_c_plus.released.connect(lambda: self.gui_button_jog(0, 'c', 1))
        self.w.jog_c_minus.pressed.connect(lambda: self.gui_button_jog(1, 'c', -1))
        self.w.jog_c_minus.released.connect(lambda: self.gui_button_jog(0, 'c', -1))
        self.w.cut_rec_fwd.pressed.connect(lambda: self.cutrec_motion(1))
        self.w.cut_rec_fwd.released.connect(lambda: self.cutrec_motion(0))
        self.w.cut_rec_rev.pressed.connect(lambda: self.cutrec_motion(-1))
        self.w.cut_rec_rev.released.connect(lambda: self.cutrec_motion(0))
        self.w.cut_rec_cancel.pressed.connect(lambda: self.cutrec_cancel_pressed(1))
        self.w.cut_rec_n.pressed.connect(lambda: self.cutrec_move(1, 0, 1))
        self.w.cut_rec_ne.pressed.connect(lambda: self.cutrec_move(1, 1, 1))
        self.w.cut_rec_e.pressed.connect(lambda: self.cutrec_move(1, 1, 0))
        self.w.cut_rec_se.pressed.connect(lambda: self.cutrec_move(1, 1, -1))
        self.w.cut_rec_s.pressed.connect(lambda: self.cutrec_move(1, 0, -1))
        self.w.cut_rec_sw.pressed.connect(lambda: self.cutrec_move(1, -1, -1))
        self.w.cut_rec_w.pressed.connect(lambda: self.cutrec_move(1, -1, 0))
        self.w.cut_rec_nw.pressed.connect(lambda: self.cutrec_move(1, -1, 1))
        self.xOffsetPin.value_changed.connect(lambda v: self.cutrec_offset_changed(v, self.yOffsetPin.get()))
        self.yOffsetPin.value_changed.connect(lambda v: self.cutrec_offset_changed(self.xOffsetPin.get(), v))
        self.offsetsActivePin.value_changed.connect(lambda v: self.offsets_active_changed(v))
        self.consChangePin.value_changed.connect(lambda v: self.consumable_change_changed(v))
        self.w.cam_mark.clicked.connect(self.cam_mark_clicked)
        self.w.cam_goto.clicked.connect(self.cam_goto_clicked)
        self.w.cam_zoom_plus.pressed.connect(self.cam_zoom_plus_pressed)
        self.w.cam_zoom_minus.pressed.connect(self.cam_zoom_minus_pressed)
        self.w.cam_dia_plus.pressed.connect(self.cam_dia_plus_pressed)
        self.w.cam_dia_minus.pressed.connect(self.cam_dia_minus_pressed)
        self.w.view_p.pressed.connect(self.view_p_pressed)
        self.w.view_z.pressed.connect(lambda: self.view_z_pressed(self.w.gcodegraphics))
        self.w.view_t.pressed.connect(lambda: self.view_t_pressed(self.w.gcodegraphics))
        self.w.view_clear.pressed.connect(self.view_clear_pressed)
        self.w.pan_left.pressed.connect(lambda: self.pan_left_pressed(self.w.gcodegraphics))
        self.w.pan_right.pressed.connect(lambda: self.pan_right_pressed(self.w.gcodegraphics))
        self.w.pan_up.pressed.connect(lambda: self.pan_up_pressed(self.w.gcodegraphics))
        self.w.pan_down.pressed.connect(lambda: self.pan_down_pressed(self.w.gcodegraphics))
        self.w.zoom_in.pressed.connect(lambda: self.zoom_in_pressed(self.w.gcodegraphics))
        self.w.zoom_out.pressed.connect(lambda: self.zoom_out_pressed(self.w.gcodegraphics))
        self.w.conv_view_z.pressed.connect(lambda: self.view_z_pressed(self.w.conv_preview))
        self.w.conv_view_t.pressed.connect(lambda: self.view_t_pressed(self.w.conv_preview))
        self.w.conv_pan_left.pressed.connect(lambda: self.pan_left_pressed(self.w.conv_preview))
        self.w.conv_pan_right.pressed.connect(lambda: self.pan_right_pressed(self.w.conv_preview))
        self.w.conv_pan_up.pressed.connect(lambda: self.pan_up_pressed(self.w.conv_preview))
        self.w.conv_pan_down.pressed.connect(lambda: self.pan_down_pressed(self.w.conv_preview))
        self.w.conv_zoom_in.pressed.connect(lambda: self.zoom_in_pressed(self.w.conv_preview))
        self.w.conv_zoom_out.pressed.connect(lambda: self.zoom_out_pressed(self.w.conv_preview))
        self.w.camera.pressed.connect(self.camera_pressed)
        self.w.laser.pressed.connect(self.laser_pressed)
        self.w.laser.clicked.connect(self.laser_clicked)
        self.w.main_tab_widget.currentChanged.connect(lambda w: self.main_tab_changed(w))
        self.zHeightPin.value_changed.connect(lambda v: self.z_height_changed(v))
        self.plasmacStatePin.value_changed.connect(lambda v: self.plasmac_state_changed(v))
        self.plasmacStopPin.value_changed.connect(lambda v: self.plasmac_stop_changed(v))
        self.w.feed_label.pressed.connect(self.feed_label_pressed)
        self.w.rapid_label.pressed.connect(self.rapid_label_pressed)
        self.w.jogs_label.pressed.connect(self.jogs_label_pressed)
        self.paramTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.PARAMETERS, not v))
        self.settingsTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.SETTINGS, not v))
        self.convTabDisable.value_changed.connect(lambda v: self.w.main_tab_widget.setTabVisible(self.CONVERSATIONAL, not v))
        self.w.cut_time_reset.pressed.connect(lambda: self.statistic_reset('cut_time', 'Cut time'))
        self.w.probe_time_reset.pressed.connect(lambda: self.statistic_reset('probe_time', 'Probe time'))
        self.w.paused_time_reset.pressed.connect(lambda: self.statistic_reset('paused_time', 'Paused time'))
        self.w.run_time_reset.pressed.connect(lambda: self.statistic_reset('run_time', 'Program run time'))
        self.w.torch_time_reset.pressed.connect(lambda: self.statistic_reset('torch_time', 'Torch on time'))
        self.w.rapid_time_reset.pressed.connect(lambda: self.statistic_reset('rapid_time', 'Rapid time'))
        self.w.cut_length_reset.pressed.connect(lambda: self.statistic_reset('cut_length', 'Cut length'))
        self.w.pierce_reset.pressed.connect(lambda: self.statistic_reset('pierce_count', 'Pierce count'))
        self.w.all_reset.pressed.connect(self.statistics_reset)
        self.extPowerPin.value_changed.connect(lambda v: self.power_button("external", v))
        self.extRunPin.value_changed.connect(lambda v: self.ext_run(v))
        self.extPausePin.value_changed.connect(lambda v: self.ext_pause(v))
        self.extPauseOnlyPin.value_changed.connect(lambda v: self.ext_pause_only(v))
        self.extResumePin.value_changed.connect(lambda v: self.ext_resume(v))
        self.extAbortPin.value_changed.connect(lambda v: self.ext_abort(v))
        self.extTouchOffPin.value_changed.connect(lambda v: self.ext_touch_off(v))
        self.extLaserTouchOffPin.value_changed.connect(lambda v: self.ext_laser_touch_off(v))
        self.extLaserTogglePin.value_changed.connect(lambda v: self.ext_laser_toggle(v))
        self.extRunPausePin.value_changed.connect(lambda v: self.ext_run_pause(v))
        self.extHeightOvrPlusPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, 1))
        self.extHeightOvrMinusPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, -1))
        self.extHeightOvrResetPin.value_changed.connect(lambda v: self.height_ovr_pressed(v, 0))
        self.extHeightOvrCountsPin.value_changed.connect(lambda v: self.height_ovr_encoder(v))
        self.extHeightOvrScalePin.value_changed.connect(lambda v: self.height_ovr_scale_change(v))
        self.extChangeConsPin.value_changed.connect(lambda v: self.ext_change_consumables(v))
        self.extCutRecRevPin.value_changed.connect(lambda v: self.cutrec_motion(-v))
        self.extCutRecFwdPin.value_changed.connect(lambda v: self.cutrec_motion(v))
        self.extCutRecNPin.value_changed.connect(lambda v: self.cutrec_move(v, 0, 1))
        self.extCutRecNEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, 1))
        self.extCutRecEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, 0))
        self.extCutRecSEPin.value_changed.connect(lambda v: self.cutrec_move(v, 1, -1))
        self.extCutRecSPin.value_changed.connect(lambda v: self.cutrec_move(v, 0, -1))
        self.extCutRecSWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, -1))
        self.extCutRecWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, 0))
        self.extCutRecNWPin.value_changed.connect(lambda v: self.cutrec_move(v, -1, 1))
        self.extCutRecCancelPin.value_changed.connect(lambda v: self.cutrec_cancel_pressed(v))
        self.extTorchEnablePin.value_changed.connect(lambda v: self.ext_torch_enable_changed(v))
        self.extThcEnablePin.value_changed.connect(lambda v: self.ext_thc_enable_changed(v))
        self.extCornerLockEnablePin.value_changed.connect(lambda v: self.ext_corner_lock_enable_changed(v))
        self.extVoidLockEnablePin.value_changed.connect(lambda v: self.ext_void_lock_enable_changed(v))
        self.extIgnoreArcOkPin.value_changed.connect(lambda v: self.ext_ignore_arc_ok_changed(v))
        self.extMeshModePin.value_changed.connect(lambda v: self.ext_mesh_mode_changed(v))
        self.extOhmicProbeEnablePin.value_changed.connect(lambda v: self.ext_ohmic_probe_enable_changed(v))
        self.extAutoVoltsEnablePin.value_changed.connect(lambda v: self.ext_auto_volts_enable_changed(v))
        self.extJogSlowPin.value_changed.connect(self.ext_jog_slow)
        self.extProbePin.value_changed.connect(lambda v: self.ext_probe_test(v))
        self.extPulsePin.value_changed.connect(lambda v: self.ext_torch_pulse(v))
        self.extOhmicPin.value_changed.connect(lambda v: self.ext_ohmic_test(v))
        self.extFramingPin.value_changed.connect(lambda v: self.ext_frame_job(v))
        self.probeTestErrorPin.value_changed.connect(lambda v: self.probe_test_error(v))
        self.w.preview_stack.currentChanged.connect(self.preview_stack_changed)
        self.w.gcode_stack.currentChanged.connect(self.gcode_stack_changed)
        click_signal(self.w.material_label).connect(self.show_material_selector)
        click_signal(self.w.velocity_label).connect(self.show_material_selector)
        click_signal(self.w.velocity_show).connect(self.show_material_selector)
        self.w.conv_line.pressed.connect(lambda: self.conv_call('line'))
        self.w.conv_circle.pressed.connect(lambda: self.conv_call('circle'))
        self.w.conv_ellipse.pressed.connect(lambda: self.conv_call('ellipse'))
        self.w.conv_triangle.pressed.connect(lambda: self.conv_call('triangle'))
        self.w.conv_rectangle.pressed.connect(lambda: self.conv_call('rectangle'))
        self.w.conv_polygon.pressed.connect(lambda: self.conv_call('polygon'))
        self.w.conv_bolt.pressed.connect(lambda: self.conv_call('bolt'))
        self.w.conv_slot.pressed.connect(lambda: self.conv_call('slot'))
        self.w.conv_star.pressed.connect(lambda: self.conv_call('star'))
        self.w.conv_gusset.pressed.connect(lambda: self.conv_call('gusset'))
        self.w.conv_sector.pressed.connect(lambda: self.conv_call('sector'))
        self.w.conv_block.pressed.connect(lambda: self.conv_call('block'))
        self.w.conv_new.pressed.connect(lambda: self.conv_call('new'))
        self.w.conv_save.pressed.connect(lambda: self.conv_call('save'))
        self.w.conv_settings.pressed.connect(lambda: self.conv_call('settings'))
        self.w.conv_send.pressed.connect(lambda: self.conv_call('send'))
        self.w.chk_override_jog.stateChanged.connect(self.override_jog_changed)
        self.jogInhibited.value_changed.connect(lambda v: self.jog_inhibited_changed(v))
        self.sensorActive.value_changed.connect(lambda v: self.sensor_active_changed(v))
        self.zOffsetPin.value_changed.connect(lambda v: self.z_offset_changed(v))
        self.laserRecStatePin.value_changed.connect(lambda v: self.laser_recovery_state_changed(v))
        self.ohmicLedInPin.value_changed.connect(lambda v: self.ohmic_sensed(v))
        self.w.webview_back.pressed.connect(self.web_back_pressed)
        self.w.webview_forward.pressed.connect(self.web_forward_pressed)
        self.w.webview_reload.pressed.connect(self.web_reload_pressed)
        self.velReduct.value_changed.connect(lambda: self.vel_reduct())

    def conv_call(self, operation):
        if self.developmentPin.get():
            reload(self.CONV)
        if operation == 'block':
            self.CONV.conv_block_pressed(self, self.w)
        elif operation == 'new':
            self.CONV.conv_new_pressed(self, self.w, 'button')
        elif operation == 'save':
            self.CONV.conv_save_pressed(self, self.w)
        elif operation == 'settings':
            self.CONV.conv_settings_pressed(self, self.w)
        elif operation == 'send':
            self.CONV.conv_send_pressed(self, self.w)
        else:
            self.CONV.conv_shape_request(self, self.w, f'conv_{operation}')

    def set_axes_and_joints(self):
        self.coordinates = 'xyz'  # backup in case we cannot find valid coordinates
        kinematics = self.iniFile.find('KINS', 'KINEMATICS').lower().split() or None
        if not kinematics:
            head = _translate('HandlerClass', 'INI File Error')
            msg0 = _translate('HandlerClass', 'Error in [KINS]KINEMATICS in the INI file')
            msg1 = _translate('HandlerClass', 'reverting to default coordinates of xyz')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
        else:
            coords = [s for s in kinematics if 'coordinates' in s]
            if coords:
                self.coordinates = coords[0].split('=')[1].strip()
            else:
                coords = self.iniFile.find('KINS', 'COORDINATES').lower() or None
                if coords:
                    self.coordinates = coords
        # hide axes a, b, and c if not being used
        for axis in 'abc':
            if axis not in self.axes['valid']:
                for i in self.axes[axis]:
                    self.w[i].hide()
        # setup home buttons
        for axis in self.axes['valid']:
            self.w[f'home_{axis}'].set_joint(self.coordinates.index(axis))
            self.w[f'home_{axis}'].set_joint_number(self.coordinates.index(axis))
        for joint in range(len(self.coordinates)):
            # check if home all button required
            if not self.iniFile.find(f'JOINT_{joint}', 'HOME_SEQUENCE'):
                self.w.home_all.hide()
            # check if not joggable before homing
            elif self.iniFile.find(f'JOINT_{joint}', 'HOME_SEQUENCE').startswith('-'):
                if f'jog_{self.coordinates[joint]}_plus' not in self.jogSyncList:
                    self.jogSyncList.append(f'jog_{self.coordinates[joint]}_plus')
                    self.jogSyncList.append(f'jog_{self.coordinates[joint]}_minus')
                    self.jogButtonList.remove(f'jog_{self.coordinates[joint]}_plus')
                    self.jogButtonList.remove(f'jog_{self.coordinates[joint]}_minus')
        # remove 'all_homed' requirement for buttons in self.jogButtonList
        for jogButton in self.jogButtonList:
            andRules = self.interlockRules[jogButton].get('and', {})
            if 'all_homed' in andRules:
                del andRules['all_homed']

    def set_mode(self):
        block1 = ['arc_ok_high', 'arc_ok_high_lbl', 'arc_ok_low', 'arc_ok_low_lbl']
        block2 = ['arc_voltage_scale', 'arc_voltage_scale_lbl', 'arc_voltage_offset', 'arc_voltage_offset_lbl',
                  'voidlock_frm', 'height_per_volt', 'height_per_volt_lbl', 'thc_delay', 'thc_delay_lbl',
                  'thc_sample_counts', 'thc_sample_counts_lbl', 'thc_sample_threshold', 'thc_sample_threshold_lbl',
                  'thc_threshold', 'thc_threshold_lbl', 'pid_i_gain', 'pid_i_gain_lbl', 'pid_d_gain', 'pid_d_gain_lbl',
                  'use_auto_volts', 'use_auto_volts_lbl', 'led_thc_active', 'led_thc_active_lbl', 'arc_voltage',
                  'arc_override_frm', 'voidlock_slope', 'voidlock_slope_lbl', 'thc_auto', 'thc_auto_lbl']
        if self.mode == 1:
            hal.set_p('plasmac.mode', '1')
            for widget in block1:
                self.w[widget].hide()
        elif self.mode == 2:
            hal.set_p('plasmac.mode', '2')
            for widget in block1 + block2:
                self.w[widget].hide()
                self.w.pid_p_gain_lbl.setText(_translate('HandlerClass', 'Speed %'))

    def set_spinbox_parameters(self):
        if self.units == 'in':
            self.w.setup_feed_rate.setRange(4.0, int(self.thcFeedRate))
            self.w.setup_feed_rate.setDecimals(1)
            self.w.setup_feed_rate.setSingleStep(0.1)
            self.w.safe_height.setRange(0, int(self.maxHeight))
            self.w.safe_height.setDecimals(3)
            self.w.safe_height.setSingleStep(0.01)
            self.w.probe_feed_rate.setRange(4.0, int(self.thcFeedRate))
            self.w.probe_feed_rate.setDecimals(1)
            self.w.probe_feed_rate.setSingleStep(0.1)
            self.w.probe_start_height.setRange(0, int(self.maxHeight))
            self.w.probe_start_height.setDecimals(3)
            self.w.probe_start_height.setSingleStep(0.01)
            self.w.offset_feed_rate.setRange(4.0, int(self.offsetFeedRate))
            self.w.offset_feed_rate.setDecimals(1)
            self.w.offset_feed_rate.setSingleStep(0.1)
            self.w.float_switch_travel.setRange(-1.0, 1.0)
            self.w.float_switch_travel.setDecimals(3)
            self.w.float_switch_travel.setSingleStep(0.001)
            self.w.height_per_volt.setRange(0.001, 0.01)
            self.w.height_per_volt.setDecimals(3)
            self.w.height_per_volt.setSingleStep(0.001)
            self.w.ohmic_probe_offset.setRange(-1.0, 1.0)
            self.w.ohmic_probe_offset.setDecimals(3)
            self.w.ohmic_probe_offset.setSingleStep(0.001)
            self.w.skip_ihs_distance.setRange(0.0, 40.0)
            self.w.skip_ihs_distance.setDecimals(1)
            self.w.skip_ihs_distance.setSingleStep(0.1)
            self.w.kerf_width.setRange(0.0, 1.0)
            self.w.kerf_width.setDecimals(3)
            self.w.kerf_width.setSingleStep(0.001)
            self.w.cut_feed_rate.setRange(0.0, 999.0)
            self.w.cut_feed_rate.setDecimals(1)
            self.w.cut_feed_rate.setSingleStep(0.1)
            self.w.cut_height.setRange(0.0, 1.0)
            self.w.cut_height.setDecimals(3)
            self.w.cut_height.setSingleStep(0.001)
            self.w.pierce_height.setRange(0.0, 1.0)
            self.w.pierce_height.setDecimals(3)
            self.w.pierce_height.setSingleStep(0.001)
            self.w.x_pierce_offset.setDecimals(3)
            self.w.x_pierce_offset.setRange(-0.2, 0.2)
            self.w.x_pierce_offset.setSingleStep(0.01)
            self.w.y_pierce_offset.setDecimals(3)
            self.w.y_pierce_offset.setRange(-0.2, 0.2)
            self.w.y_pierce_offset.setSingleStep(0.01)
            self.w.material_thickness.setRange(0.0, int(self.maxHeight))
            self.w.material_thickness.setDecimals(3)
            self.w.material_thickness.setSingleStep(0.001)
            self.w.slat_height.setRange(0.0, int(self.maxHeight))
            self.w.slat_height.setDecimals(3)
            self.w.slat_height.setSingleStep(0.001)
        else:
            self.w.setup_feed_rate.setMaximum(int(self.thcFeedRate))
            self.w.safe_height.setMaximum(int(self.maxHeight))
            self.w.probe_feed_rate.setMaximum(int(self.thcFeedRate))
            self.w.probe_start_height.setMaximum(int(self.maxHeight))
            self.w.offset_feed_rate.setMaximum(int(self.offsetFeedRate))
            self.w.material_thickness.setMaximum(int(self.maxHeight))
            self.w.slat_height.setMaximum(int(self.maxHeight))

    def set_probe_offset_pins(self):
        hal.set_p('plasmac.offset-probe-x', f'{self.probeOffsetX}')
        hal.set_p('plasmac.offset-probe-y', f'{self.probeOffsetY}')
        hal.set_p('plasmac.offset-probe-delay', f'{self.probeDelay}')

    def kb_jog(self, state, joint, direction, shift=False, linear=True):
        if linear:
            distance = STATUS.get_jog_increment()
            rate = STATUS.get_jograte()/60
        else:
            distance = STATUS.get_jog_increment_angular()
            rate = STATUS.get_jograte_angular()/60
        if state:
            if not STATUS.is_man_mode() or not STATUS.machine_is_on() or \
               (self.offsetsActivePin.get() and not self.manualCut):
                return
            if (shift or self.jogFast) and not self.manualCut:
                rate = INFO.MAX_LINEAR_JOG_VEL
            elif self.jogSlow and not self.w.jog_slow.isChecked():
                rate = STATUS.get_jograte()/60/self.slowJogFactor
            ACTION.JOG(joint, direction, rate, distance)
            self.w.grabKeyboard()
        else:
            self.w.releaseKeyboard()
            if not STATUS.get_jog_increment():
                ACTION.JOG(joint, 0, 0, 0)

    def keyboard_shortcuts(self):
        if self.w.chk_keyboard_shortcuts.isChecked():
            return True
        else:
            return False

    def soft_keyboard(self):
        if self.w.chk_soft_keyboard.isChecked():
            inputType = 'CALCULATOR'
            self.w.originoffsetview.setProperty('dialog_code_string', 'CALCULATOR')
            self.w.originoffsetview.setProperty('text_dialog_code_string', 'KEYBOARD')
            self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 4)
            self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 4)
            self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 4)
            self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 4)
            self.vkb_check()
            if self.w.main_tab_widget.currentIndex() == self.PARAMETERS:
                self.vkb_show(True)
            elif self.w.main_tab_widget.currentIndex() == self.SETTINGS:
                self.vkb_show()
            self.w.chk_keyboard_shortcuts.setChecked(False)
            self.w.chk_keyboard_shortcuts.setEnabled(False)
        else:
            inputType = 'ENTRY'
            self.w.originoffsetview.setProperty('dialog_code_string', '')
            self.w.originoffsetview.setProperty('text_dialog_code_string', '')
            self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 1)
            self.w.gcode_display.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 1)
            self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRAASCENT, 1)
            self.w.gcode_editor.editor.SendScintilla(QsciScintilla.SCI_SETEXTRADESCENT, 1)
            self.vkb_hide()
            self.w.chk_keyboard_shortcuts.setEnabled(True)
        for axis in 'xyzabc':
            button = f'touch_{axis}'
            self.w[button].dialog_code = inputType

    def overlay_update(self, state):
        self.w.gcodegraphics.update()
        self.w.conv_preview.update()

    def dialog_show_ok(self, icon, title, error, bText=_translate('HandlerClass', 'OK')):
        msg = QMessageBox(self.w)
        buttonY = msg.addButton(QMessageBox.Yes)
        buttonY.setText(bText)
        buttonY.setIcon(QIcon())
        msg.setIcon(icon)
        msg.setWindowTitle(title)
        msg.setText(error)
        msg.setWindowFlag(Qt.Popup)
        msg.exec_()
        self.dialogError = False
        return msg

    def dialog_show_yesno(self, icon, title, error, bY=_translate('HandlerClass', 'YES'), bN=_translate('HandlerClass', 'NO')):
        msg = QMessageBox(self.w)
        buttonY = msg.addButton(QMessageBox.Yes)
        buttonY.setText(bY)
        buttonY.setIcon(QIcon())
        buttonN = msg.addButton(QMessageBox.No)
        buttonN.setText(bN)
        buttonN.setIcon(QIcon())
        msg.setIcon(icon)
        msg.setWindowTitle(title)
        msg.setText(error)
        msg.setWindowFlag(Qt.WindowStaysOnTopHint) if 'shutdown' in error else msg.setWindowFlag(Qt.Popup)
        choice = msg.exec_()
        if choice == QMessageBox.Yes:
            return True
        else:
            return False

    # virtkb: 0=none, 1=alpha~close, 2=num~close, 3=alpha~num, 4=num~num, 5=alpha~alpha, 6=num~alpha
    def dialog_input(self, virtkb, title, text, btn1, btn2, delay=None):
        dialog = QInputDialog(self.w)
        dialog.setWindowTitle(title)
        dialog.setLabelText(f'{text}')
        if btn1:
            dialog.setOkButtonText(btn1)
        if btn2:
            dialog.setCancelButtonText(btn2)
        if delay is not None:
            dialog.setTextValue(f'{delay:0.2f}')
        for button in dialog.findChildren(QPushButton):
            button.setIcon(QIcon())
        if virtkb in (1, 3, 5):
            self.vkb_show(False)
        elif virtkb in (2, 4, 6):
            self.vkb_show(True)
        valid = dialog.exec_()
        if virtkb < 3:
            self.vkb_hide()
        elif virtkb in (3, 4):
            self.vkb_show(True)
        elif virtkb in (5, 6):
            self.vkb_show(False)
        out = dialog.textValue()
        return valid, out

    def dialog_run_from_line(self):
        rFl = QDialog(self.w)
        rFl.setWindowTitle(_translate('HandlerClass', 'Run From Line'))
        lbl1 = QLabel(_translate('HandlerClass', 'USE LEADIN:'))
        lbl2 = QLabel(_translate('HandlerClass', 'LEADIN LENGTH:'))
        lbl3 = QLabel(_translate('HandlerClass', 'LEADIN ANGLE:'))
        lbl4 = QLabel('')
        leadinDo = QCheckBox()
        leadinLength = QDoubleSpinBox()
        leadinAngle = QDoubleSpinBox()
        buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
        buttonBox = QDialogButtonBox(buttons)
        buttonBox.accepted.connect(rFl.accept)
        buttonBox.rejected.connect(rFl.reject)
        buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'LOAD'))
        buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon())
        buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL'))
        buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon())
        layout = QGridLayout()
        layout.addWidget(lbl1, 0, 0)
        layout.addWidget(lbl2, 1, 0)
        layout.addWidget(lbl3, 2, 0)
        layout.addWidget(lbl4, 3, 0)
        layout.addWidget(leadinDo, 0, 1)
        layout.addWidget(leadinLength, 1, 1)
        layout.addWidget(leadinAngle, 2, 1)
        layout.addWidget(buttonBox, 4, 0, 1, 2)
        rFl.setLayout(layout)
        lbl1.setAlignment(Qt.AlignRight | Qt.AlignBottom)
        lbl2.setAlignment(Qt.AlignRight | Qt.AlignBottom)
        lbl3.setAlignment(Qt.AlignRight | Qt.AlignBottom)
        if self.units == 'in':
            leadinLength.setDecimals(2)
            leadinLength.setSingleStep(0.05)
            leadinLength.setSuffix(' inch')
            leadinLength.setMinimum(0.05)
        else:
            leadinLength.setDecimals(0)
            leadinLength.setSingleStep(1)
            leadinLength.setSuffix(' mm')
            leadinLength.setMinimum(1)
        leadinAngle.setDecimals(0)
        leadinAngle.setSingleStep(1)
        leadinAngle.setSuffix(' deg')
        leadinAngle.setRange(-359, 359)
        leadinAngle.setWrapping(True)
        self.vkb_show(True)
        result = rFl.exec_()
        self.vkb_hide()
        # load clicked
        if result:
            return {'cancel': False, 'do': leadinDo.isChecked(), 'length': leadinLength.value(), 'angle': leadinAngle.value()}
        # cancel clicked
        else:
            return {'cancel': True}

    def dialog_rfl_type(self):
        rflT = QDialog(self.w)
        rflT.setWindowTitle(_translate('HandlerClass', 'Run From Line'))
        run = QRadioButton(_translate('HandlerClass', 'HERE TO END'))
        cut = QRadioButton(_translate('HandlerClass', 'THIS CUTPATH'))
        lbl = QLabel('')
        buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
        buttonBox = QDialogButtonBox(buttons)
        buttonBox.accepted.connect(rflT.accept)
        buttonBox.rejected.connect(rflT.reject)
        buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'OK'))
        buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon())
        buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL'))
        buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon())
        layout = QGridLayout()
        layout.addWidget(run, 0, 0)
        layout.addWidget(cut, 1, 0)
        layout.addWidget(lbl, 2, 0)
        layout.addWidget(buttonBox, 3, 0)
        rflT.setLayout(layout)
        run.setChecked(True)
        self.vkb_show(True)
        result = rflT.exec_()
        self.vkb_hide()
        # ok clicked
        if result:
            if cut.isChecked():
                type_ = 'cut'
            else:
                type_ = 'end'
            return {'cancel': False, 'type': type_}
        # cancel clicked
        else:
            return {'cancel': True, 'type': 'end'}

    def show_cut_critical_dialog(self, rcButtonList):
        checkStyle = 'QCheckBox::indicator { margin-left: 8px; margin-right: 8px; }\n \
                        QCheckBox { font-size: 11pt; }'
        ccr = QDialog(self.w)
        ccr.setWindowTitle(_translate('HandlerClass', 'Untoggled Cut Critical Buttons'))
        icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxWarning)
        iconLabel = QLabel()
        iconLabel.setPixmap(icon.pixmap(32, 32))
        msg0 = _translate('HandlerClass', 'The following buttons have not been toggled')
        msg1 = _translate('HandlerClass', 'Select items to be toggled when CONTINUE is clicked')
        lbl0 = QLabel(f'\n{msg0}:\n')
        lbl1 = QLabel('')
        lbl2 = QLabel(f'\n{msg1}\n')
        lbl2.setStyleSheet("padding-left: 1px;")
        buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttonBox.accepted.connect(ccr.accept)
        buttonBox.rejected.connect(ccr.reject)
        buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'CONTINUE'))
        buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon())
        buttonBox.button(QDialogButtonBox.Ok).setMinimumWidth(120)
        buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL'))
        buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon())
        hLayout = QHBoxLayout()
        hLayout.addWidget(iconLabel)
        hLayout.addWidget(lbl0)
        hLayout.addStretch()
        vLayout = QVBoxLayout()
        vLayout.addLayout(hLayout)
        checkBoxes = []
        for bText in rcButtonList:
            checkBox = QCheckBox(bText)
            checkBox.setStyleSheet(checkStyle)
            vLayout.addWidget(checkBox)
            checkBoxes.append(checkBox)
        if len(rcButtonList) > 1:
            toggleAll = QCheckBox(_translate('HandlerClass', 'TOGGLE ALL'))
            toggleAll.setStyleSheet(checkStyle)
            vLayout.addWidget(lbl1)
            vLayout.addWidget(toggleAll)
            toggleAll.stateChanged.connect(lambda state: [checkBox.setChecked(state == Qt.Checked) for checkBox in checkBoxes])
        vLayout.addWidget(lbl2)
        vLayout.addWidget(buttonBox)
        ccr.setLayout(vLayout)
        result = ccr.exec_()
        checkList = [checkBox.text() for checkBox in checkBoxes if checkBox.isChecked()]
        return result, checkList

    def invert_pin_state(self, halpin):
        if 'qtplasmac.ext_out_' in halpin:
            pin = f'out{halpin.split("out_")[1]}Pin'
            self[pin].set(not hal.get_value(halpin))
        else:
            hal.set_p(halpin, str(not hal.get_value(halpin)))
        self.set_button_color()

    def set_button_color(self):
        for halpin in self.halTogglePins:
            if hal.get_value(halpin):
                if self.button_normal_check(self.halTogglePins[halpin][0]):
                    self.button_active(self.halTogglePins[halpin][0])
                text = 3
            else:
                if not self.button_normal_check(self.halTogglePins[halpin][0]):
                    self.button_normal(self.halTogglePins[halpin][0])
                text = 2
            if self.halTogglePins[halpin][3]:
                toggleText = self.halTogglePins[halpin][text].replace('\\', '\n')
                self.w[self.halTogglePins[halpin][0]].setText(f'{toggleText}')
        for halpin in self.halPulsePins:
            if hal.get_value(halpin):
                if self.button_normal_check(self.halPulsePins[halpin][0]):
                    self.button_active(self.halPulsePins[halpin][0])
            else:
                if not self.button_normal_check(self.halPulsePins[halpin][0]):
                    self.button_normal(self.halPulsePins[halpin][0])
        if self.tlButton:
            for button in self.tlButton:
                if self.laserOnPin.get():
                    if self.button_normal_check(button):
                        self.button_active(button)
                else:
                    if not self.button_normal_check(button):
                        self.button_normal(button)

    def cut_critical_toggle_check(self):
        # self.halTogglePins format is: button name, run critical flag, button text, alt button text
        checkDict = {
            self.halTogglePins[halpin][2].replace('\n', ' '): halpin
            for halpin in self.halTogglePins
            if self.halTogglePins[halpin][1] and not hal.get_value(halpin)
        }
        if checkDict and self.w.torch_enable.isChecked():
            result, checked = self.show_cut_critical_dialog(list(checkDict.keys()))
            if result:
                for bText in checked:
                    halpin = checkDict[bText]
                    self.user_button_down(int(self.halTogglePins[halpin][0].replace('button_','')))
            return result
        return True

    def preview_stack_changed(self):
        index = self.w.preview_stack.currentIndex()
        if index == self.PREVIEW:
            self.autorepeat_keys(False)
        elif index == self.OPEN:
            self.button_active(self.w.file_open.objectName())
            self.autorepeat_keys(True)
            self.vkb_hide()
            self.w.filemanager.table.setFocus()
        elif index == self.EDIT:
            self.button_active(self.w.file_edit.objectName())
            text0 = _translate('HandlerClass', 'EDIT')
            text1 = _translate('HandlerClass', 'CLOSE')
            self.w.file_edit.setText(f'{text0}\n{text1}')
            self.autorepeat_keys(True)
        elif index == self.CAMERA:
            self.button_active('camera')
            self.cameraOn = True
        elif index == self.OFFSETS:
            self.button_active(self.ovButton)
            for row in range(self.w.originoffsetview.tablemodel.rowCount(self.w.originoffsetview)):
                self.w.originoffsetview.resizeRowToContents(row)
            for column in range(self.w.originoffsetview.tablemodel.rowCount(self.w.originoffsetview)-1):
                self.w.originoffsetview.resizeColumnToContents(column)
        elif index == self.USER_MANUAL:
            self.button_active(self.umButton)
            self.autorepeat_keys(True)
        if index != self.OPEN or index == self.PREVIEW:
            self.button_normal(self.w.file_open.objectName())
        if index != self.EDIT or index == self.PREVIEW:
            self.button_normal(self.w.file_edit.objectName())
            self.w.file_edit.setText(_translate('HandlerClass', 'EDIT'))
        if index != self.CAMERA or index == self.PREVIEW:
            self.button_normal('camera')
        if index != self.OFFSETS or index == self.PREVIEW:
            if self.ovButton:
                self.button_normal(self.ovButton)
        if index != self.USER_MANUAL or index == self.PREVIEW:
            if self.umButton:
                self.button_normal(self.umButton)
        self.refresh_button_states()

    def gcode_stack_changed(self):
        if self.w.gcode_stack.currentIndex() == self.MDI:
            self.button_active(self.w.mdi_show.objectName())
            text0 = _translate('HandlerClass', 'MDI')
            text1 = _translate('HandlerClass', 'CLOSE')
            self.w.mdi_show.setText(f'{text0}\n{text1}')
            self.w.mdihistory.reload()
            self.w.mdihistory.MDILine.setFocus()
            self.autorepeat_keys(True)
        else:
            self.button_normal(self.w.mdi_show.objectName())
            self.w.mdi_show.setText(_translate('HandlerClass', 'MDI'))
            if self.w.preview_stack.currentIndex() != self.EDIT:
                self.autorepeat_keys(False)
        self.refresh_button_states()

    def set_mc_states(self, state):
        if self.manualCut:
            self.jogPreManCut[0] = self.w.jog_slow.isChecked()
            self.jogPreManCut[1] = self.w.jog_slider.value()
            self.jogPreManCut[2] = self.w.jogincrements.currentIndex()
            if self.w.jog_slow.isChecked():
                self.jog_slow_pressed(True)
            self.w.jog_slider.setValue(self.w.cut_feed_rate.value())
            self.w.jogincrements.setCurrentIndex(0)
        else:
            if self.jogPreManCut[0]:
                self.jog_slow_pressed(True)
            self.w.jog_slider.setValue(self.jogPreManCut[1])
            self.w.jogincrements.setCurrentIndex(self.jogPreManCut[2])
        self.refresh_button_states()

    def show_material_selector(self):
        if STATUS.is_interp_idle():
            self.w.material_selector.showPopup()

    def autorepeat_keys(self, state):
        if not self.autorepeat_skip:
            if state:
                ACTION.ENABLE_AUTOREPEAT_KEYS(' ')
            else:
                ACTION.DISABLE_AUTOREPEAT_KEYS(' ')

    def clear_rfl(self):
        self.rflActive = False
        self.startLine = 0
        self.preRflFile = ''
        self.w.gcodegraphics.clear_highlight()

    def ohmic_sensed(self, state):
        if state:
            hal.set_p('qtplasmac.led_ohmic_probe', '1')
            self.ohmicLedTimer.start(150)

    def vm_check(self):
        try:
            response = (Popen('cat /sys/class/dmi/id/product_name', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]).decode('utf-8')
            if 'virtual' in response.lower() or 'vmware' in response.lower():
                self.virtualMachine = True
                STATUS.emit('update-machine-log', f'"{response.strip()}" Virtual Machine detected', 'TIME')
        except:
            pass

    def toggle_joint_mode(self):
        if not STATUS.is_all_homed():
            return
        if hal.get_value('halui.mode.is-teleop'):
            teleop = False
        elif hal.get_value('halui.mode.is-joint'):
            teleop = True
        ACTION.cmd.teleop_enable(teleop)
        for axis in self.axes['valid']:
            self.w[f'touch_{axis}'].setEnabled(teleop)
            self.w[f'dro_{axis}'].setEnabled(teleop)
            self.w[f'dro_label_{axis}'].setProperty('homed', teleop)
            self.w[f'dro_label_{axis}'].setStyle(self.w[f'dro_label_{axis}'].style())
        time.sleep(0.1)
        self.w.gcodegraphics.update()
        self.w.conv_preview.update()

    # called when qtplasmac.vel-reduct pin value changes, and by update_periodic every 100mS when not idle to ensure
    # the label does not get stuck due to timing discrepancies between the GUI and the component poll rates
    def vel_reduct(self):
        factor = self.velReduct.get()
        self.w.velocity_label.setText('VEL:' if factor == 1.0 else f'VEL@{factor * 100:.0f}%:')

#########################################################################################################################
# TIMER FUNCTIONS #
#########################################################################################################################

    def shutdown_timeout(self):
        self.w.close()

    def startup_timeout(self):
        if STATUS.stat.estop:
            self.estop_state(True)
        self.w.gcode_display.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.view_t_pressed(self.w.gcodegraphics)
        self.set_signal_connections()
        if self.firstRun is True:
            self.firstRun = False

    def watchdog_has_bit(self):
        states = {
            self.PROBE_HEIGHT: _translate('HandlerClass', 'moving to probe height'),
            self.PROBE_DOWN: _translate('HandlerClass', 'probing down'),
            self.PROBE_UP: _translate('HandlerClass', 'probing up'),
            self.ZERO_HEIGHT: _translate('HandlerClass', 'moving to zero height'),
            self.PIERCE_HEIGHT: _translate('HandlerClass', 'moving to pierce height'),
            self.PUDDLE_JUMP: _translate('HandlerClass', 'moving to puddle jump height'),
            self.CUT_HEIGHT: _translate('HandlerClass', 'moving to cut height'),
            self.SAFE_HEIGHT: _translate('HandlerClass', 'moving to safe height'),
            self.MAX_HEIGHT: _translate('HandlerClass', 'moving to maximum height')
        }
        head = _translate('HandlerClass', 'Z Axis Motion Timeout')
        msg0 = _translate('HandlerClass', 'Z axis did not reach the destination while')
        msg1 = _translate('HandlerClass', 'program is paused')
        msg2 = _translate('HandlerClass', 'check settings on the PARAMETERS tab')
        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} {states[self.watchDogState]}\n{msg1}\n{msg2}\n')
        ACTION.PAUSE()

    def update_periodic(self):
        if not STATUS.is_interp_idle():
            self.vel_reduct()
        self.set_button_color()
        if self.flasher:
            self.flasher -= 1
        else:
            self.flasher = self.flashRate
            self.flashState = not self.flashState
            self.flasher_timeout()
        if not self.firstRun and self.pmPort and not hal.component_exists('pmx485'):
            if self.pmx485_check(self.pmPort, True):
                self.w.gas_pressure.show()
                self.w.gas_pressure_label.show()
                self.w.cut_mode.show()
                self.w.cut_mode_label.show()
                self.w.pmx485_frame.show()
                self.w.pmx_stats_frame.show()
                self.pmx485_startup(self.pmPort)
        if not self.firstRun and os.path.isfile(self.upFile):
            exec(open(self.upFile).read())

    def flasher_timeout(self):
        if STATUS.is_auto_paused():
            if self.flashState:
                self.w.pause_resume.setText(_translate('HandlerClass', 'CYCLE RESUME'))
            else:
                self.w.pause_resume.setText('')
        elif self.w.jog_stack.currentIndex() == self.JOG:
            self.w.pause_resume.setText(_translate('HandlerClass', 'CYCLE PAUSE'))
        text = _translate('HandlerClass', 'FEED')
        if self.w.feed_slider.value() != 100:
            if self.flashState:
                self.w.feed_label.setText(f'{text}\n{STATUS.stat.feedrate * 100:.0f}%')
            else:
                self.w.feed_label.setText(' \n ')
        else:
            self.w.feed_label.setText(f'{text}\n{STATUS.stat.feedrate * 100:.0f}%')
        text = _translate('HandlerClass', 'RAPID')
        if self.w.rapid_slider.value() != 100:
            if self.flashState:
                self.w.rapid_label.setText(f'{text}\n{STATUS.stat.rapidrate * 100:.0f}%')
            else:
                self.w.rapid_label.setText(' \n ')
        else:
            self.w.rapid_label.setText(f'{text}\n{STATUS.stat.rapidrate * 100:.0f}%')
        text = _translate('HandlerClass', 'JOG')
        if self.manualCut:
            if self.flashState:
                self.w.run.setText(_translate('HandlerClass', 'MANUAL CUT'))
                self.w.jogs_label.setText(f'{text}\n{STATUS.get_jograte():.0f}')
            else:
                self.w.run.setText('')
                self.w.jogs_label.setText(' \n ')
        if self.heightOvr > 0.01 or self.heightOvr < -0.01:
            if self.flashState:
                self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}')
            else:
                self.w.height_ovr_label.setText('')
        else:
            self.w.height_ovr_label.setText(f'{self.heightOvr:.2f}')
        if self.flash_error and self.error_present:
            if self.flashState:
                self.w.error_label.setText(_translate('HandlerClass', 'ERROR SENT TO MACHINE LOG'))
            else:
                self.w.error_label.setText('')
        if self.startLine > 0:
            if not self.w.run.text().startswith(_translate('HandlerClass', 'RUN')):
                if self.flashState:
                    self.w.run.setText(self.runText)
                else:
                    self.w.run.setText('')
        elif not self.manualCut:
            self.w.run.setText(_translate('HandlerClass', 'CYCLE START'))
        if not self.w.pmx485_enable.isChecked():
            self.w.pmx485_label.setText('')
            self.pmx485LabelState = None
        elif self.pmx485CommsError:
            if self.flashState:
                self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR'))
                self.pmx485LabelState = None
            else:
                self.w.pmx485_label.setText('')
                self.pmx485LabelState = None
        elif not self.pmx485LabelState:
            if self.flashState:
                self.w.pmx485_label.setText(f'Fault Code: {self.pmx485FaultCode}')
                self.pmx485LabelState = None
            else:
                self.w.pmx485_label.setText('')
                self.pmx485LabelState = None

    def probe_timeout(self):
        if self.probeTime > 1:
            self.probeTime -= 1
            self.probeTimer.start(1000)
            self.w[self.ptButton].setText(f'{self.probeTime}')
        else:
            self.probe_test_stop()
            log = _translate('HandlerClass', 'Probe test completed')
            STATUS.emit('update-machine-log', log, 'TIME')

    def torch_timeout(self):
        self.tpRemaining = max(0.0, self.tpRemaining - 0.1)
        self.w[self.tpButton].setText(f'{self.tpRemaining:.1f}')
        if self.tpRemaining > 0:
            return
        self.tpTimer.stop()
        if not hal.get_value('plasmac.torch-pulse-hold') and not self.extPulsePin.get():
            self.torch_pulse_states(True)
            log = _translate('HandlerClass', 'Torch pulse completed')
            STATUS.emit('update-machine-log', log, 'TIME')
        else:
            text0 = _translate('HandlerClass', 'TORCH')
            text1 = _translate('HandlerClass', 'ON')
            self.w[self.tpButton].setText(f'{text0}\n{text1}')

    def pulse_timer_timeout(self):
        # halPulsePins format is: button name, pulse time, button text, remaining time, button number
        active = False
        try:
            for halpin in self.halPulsePins:
                if self.halPulsePins[halpin][3] > 0.05:
                    active = True
                    if self.halPulsePins[halpin][1] == self.halPulsePins[halpin][3]:
                        self.invert_pin_state(halpin)
                    self.halPulsePins[halpin][3] -= 0.1
                    self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][3]:0.1f}')
                elif self.w[self.halPulsePins[halpin][0]].text() != self.halPulsePins[halpin][2]:
                    self.invert_pin_state(halpin)
                    self.halPulsePins[halpin][3] = 0
                    self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][2]}')
        except Exception as err:
            self.halPulsePins[halpin][3] = 0
            bNum = self.halPulsePins[halpin][4]
            head = _translate('HandlerClass', 'HAL Pin Error')
            msg0 = _translate('HandlerClass', 'Invalid code for user button')
            msg1 = _translate('HandlerClass', 'Failed to pulse HAL pin')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n"{halpin}" {err}\n')
        if not active:
            self.pulseTimer.stop()

    def laser_timeout(self):
        if self.w.laser.isDown() or self.extLaserButton:
            if self.w.run.isEnabled() and self.laserButtonState in ['reset', 'laser']:
                framingError = self.bounds_check_framing(self.laserOffsetX, self.laserOffsetY, True)[0]
                if framingError:
                    head = _translate('HandlerClass', 'Axis Limit Error')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{framingError}\n')
                    self.laserButtonState = 'reset'
                    self.button_press_timeout('laser')
                    return
                newX = STATUS.get_position()[0][0] - STATUS.stat.g5x_offset[0] - self.laserOffsetX
                newY = STATUS.get_position()[0][1] - STATUS.stat.g5x_offset[1] - self.laserOffsetY
                units = '20' if self.gcodeProps['gcode_units'] == 'in' else '21'
                ACTION.CALL_MDI_WAIT(f'G{units} G10 L20 P0 X{newX / self.boundsMultiplier} Y{newY / self.boundsMultiplier}')
                self.laserDryRunCoords = [STATUS.stat.g5x_offset[0], STATUS.stat.g5x_offset[1]]
                self.laserOnPin.set(1)
                hal.set_p('plasmac.dry-run', '1')
                self.run_clicked()
                return
            else:
                self.laserButtonState = 'reset'
                self.button_press_timeout('laser')
            self.laserOnPin.set(0)
            self.laserButtonState = 'reset'
            self.w.laser.setText(_translate('HandlerClass', 'LASER'))

    def ohmic_led_timeout(self):
        if not self.ohmicLedInPin.get():
            hal.set_p('qtplasmac.led_ohmic_probe', '0')
        else:
            self.ohmicLedTimer.start(50)

#########################################################################################################################
# USER BUTTON FUNCTIONS #
#########################################################################################################################

    def user_button_setup(self):
        self.iniButtonCodes = ['Codes']
        iniButtonCodes = ['Codes']
        self.probePressed = False
        self.probeTime = 0
        self.probeTimer = QTimer()
        self.probeTimer.setSingleShot(True)
        self.probeTimer.timeout.connect(self.probe_timeout)
        self.tpRemaining = 0.0
        self.tpTimer = QTimer()
        self.tpTimer.setSingleShot(False)
        self.tpTimer.setInterval(100)
        self.tpTimer.timeout.connect(self.torch_timeout)
        self.pulseTime = 0
        self.pulseTimer = QTimer()
        self.pulseTimer.timeout.connect(self.pulse_timer_timeout)
        self.cutType = 0
        self.single_cut_request = False
        self.oldFile = None
        singleCodes = ['change-consumables', 'cut-type', 'framing', 'manual-cut', 'offsets-view', 'ohmic-test',
                       'probe-test', 'single-cut', 'torch-pulse', 'user-manual', 'latest-file', 'toggle-joint']
        head = _translate('HandlerClass', 'User Button Error')
        for bNum in range(1, 21):
            self.w[f'button_{bNum}'].setCheckable(False)
            bName = self.PREFS.getpref(f'{bNum} Name', '', str, 'BUTTONS') or None
            bCode = self.PREFS.getpref(f'{bNum} Code', '', str, 'BUTTONS') or None
            if bName or bCode:
                self.w[f'ub_name_{bNum}'].setText(bName)
                self.w[f'ub_code_{bNum}'].setText(bCode)
            if (bCode and not bName) or (not bCode and bName):
                msg0 = _translate('HandlerClass', 'are both required')
                msg1 = _translate('HandlerClass', 'only one has been specified for')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\nCODE + NAME {msg0}\n{msg1} BUTTON_{bNum}\n')
                self.w[f'button_{bNum}'].setText('')
                self.iniButtonCodes.append('')
                continue
            if not bCode:
                self.w[f'button_{bNum}'].setText('')
                self.iniButtonCodes.append('')
                continue
            code = bCode.lower().strip().split()[0]
            if code in singleCodes and code in iniButtonCodes:
                msg1 = _translate('HandlerClass', 'Duplicate code entry for')
                msg2 = _translate('HandlerClass', 'Using first instance only of')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg1} BUTTON_{iniButtonCodes.index(code)} + BUTTON_{bNum}\n{msg2} {bCode.split()[0]}\n')
                self.w[f'button_{bNum}'].setText('')
                self.iniButtonCodes.append('')
                continue
            self.iniButtonCodes.append(bCode)
            iniButtonCodes.append(code)
            msg0 = _translate('HandlerClass', 'Invalid code for user button')
            bNames = bName.split('\\')
            bLabel = bNames[0]
            if len(bNames) > 1:
                for name in range(1, len(bNames)):
                    bLabel += f'\n{bNames[name]}'
            self.w[f'button_{bNum}'].setText(bLabel)
            # toggle-laser can be anywhere in the button code
            if 'toggle-laser' in bCode:
                self.tlButton.append(f'button_{bNum}')
                continue
            # button code is required to start with the following codes
            if code == 'change-consumables':
                self.ccParm = bCode.replace('change-consumables', '').replace(' ', '').lower() or None
                if self.ccParm is not None and ('x' in self.ccParm or 'y' in self.ccParm):
                    self.ccButton = f'button_{bNum}'
                    self.interlockRules[self.ccButton] = self.interlockRules['change-consumables_template'].copy()
                else:
                    msg1 = _translate('HandlerClass', 'Check button code for invalid or missing arguments')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                    continue
            elif code == 'probe-test':
                if len(bCode.split()) < 3:
                    if bCode.lower().replace('probe-test', '').strip():
                        try:
                            self.ptTime = round(float(bCode.lower().replace('probe-test', '').strip()))
                        except:
                            msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                            continue
                    else:
                        self.ptTime = 10
                    self.ptButton = f'button_{bNum}'
                    self.interlockRules[self.ptButton] = self.interlockRules['probe-test_template'].copy()
                    self.probeText = self.w[self.ptButton].text()
                else:
                    msg1 = _translate('HandlerClass', 'Check button code for extra arguments')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                    continue
            elif code == 'torch-pulse':
                if len(bCode.split()) < 3:
                    if bCode.lower().replace('torch-pulse', '').strip():
                        try:
                            self.tpTime = round(float(bCode.lower().replace('torch-pulse', '').strip()), 1)
                        except:
                            msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                            continue
                        self.tpTime = 3.0 if self.tpTime > 3.0 else self.tpTime
                    else:
                        self.tpTime = 1.0
                    self.tpButton = f'button_{bNum}'
                    self.interlockRules[self.tpButton] = self.interlockRules['torch-pulse_template'].copy()
                    self.tpText = self.w[self.tpButton].text()
                else:
                    msg1 = _translate('HandlerClass', 'Check button code for extra arguments')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                    continue
            elif code == 'ohmic-test':
                self.otButton = f'button_{bNum}'
                self.interlockRules[self.otButton] = self.interlockRules['ohmic-test_template'].copy()
            elif code == 'framing':
                frButton = True
                self.defaultZ = True
                self.frFeed = 0
                bCode = bCode.lower().replace('framing', '').strip()
                if 'usecurrentzheight' in bCode:
                    bCode = bCode.lower().replace('usecurrentzheight', '').strip()
                    self.defaultZ = False
                if len(bCode):
                    if bCode[0] == 'f':
                        try:
                            self.frFeed = float(bCode.replace('f', ''))
                        except:
                            msg1 = _translate('HandlerClass', 'Check button code for invalid feed argument')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                            frButton = False
                            continue
                    else:
                        msg1 = _translate('HandlerClass', 'Check button code for invalid arguments')
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                        continue
                if frButton:
                    self.frButton = f'button_{bNum}'
                    self.interlockRules[self.frButton] = self.interlockRules['framing_template'].copy()
            elif code == 'cut-type':
                self.ctButton = f'button_{bNum}'
                self.interlockRules[self.ctButton] = self.interlockRules['cut-type_template'].copy()
            elif code == 'single-cut':
                self.scButton = f'button_{bNum}'
                self.interlockRules[self.scButton] = self.interlockRules['single-cut_template'].copy()
            elif code == 'manual-cut':
                self.mcButton = f'button_{bNum}'
                self.interlockRules[self.mcButton] = self.interlockRules['manual-cut_template'].copy()
            elif code == 'load':
                self.interlockRules[f'button_{bNum}'] = self.interlockRules['load_template'].copy()
            elif code == 'toggle-halpin':
                head = _translate('HandlerClass', 'HAL Pin Error')
                altLabel = None
                if ';;' in bCode:
                    altLabel = bCode[bCode.index(';;') + 2:].strip()
                    bCode = bCode[:bCode.index(';;')].strip()
                if len(bCode.split()) == 3 and 'cutcritical' in bCode.lower():
                    critical = True
                elif len(bCode.split()) == 2:
                    critical = False
                else:
                    head = _translate('HandlerClass', 'User Button Error')
                    msg1 = _translate('HandlerClass', 'Check button code for invalid arguments')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                    continue
                halpin = bCode.lower().split('toggle-halpin')[1].split(' ')[1].strip()
                excludedHalPins = ('plasmac.torch-pulse-start', 'plasmac.ohmic-test',
                                'plasmac.probe-test', 'plasmac.consumable-change')
                if halpin in excludedHalPins:
                    msg1 = _translate('HandlerClass', 'HAL pin')
                    msg2 = _translate('HandlerClass', 'must be toggled')
                    msg3 = _translate('HandlerClass', 'using standard button code')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n{msg3}\n')
                    continue
                else:
                    try:
                        hal.get_value(halpin)
                        self.interlockRules[f'button_{bNum}'] = self.interlockRules['toggle-halpin_template'].copy()
                    except:
                        msg1 = _translate('HandlerClass', 'HAL pin')
                        msg2 = _translate('HandlerClass', 'does not exist')
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n')
                        continue
                # halTogglePins format is: button name, run critical flag, button text, alt button text
                self.halTogglePins[halpin] = [f'button_{bNum}', critical, bLabel, altLabel]
            elif code == 'pulse-halpin':
                if len(bCode.split()) < 4:
                    try:
                        code, halpin, delay = bCode.lower().strip().split()
                    except:
                        try:
                            code, halpin = bCode.lower().strip().split()
                            delay = '1.0'
                        except:
                            head = _translate('HandlerClass', 'User Button Error')
                            msg1 = _translate('HandlerClass', 'Check button code for invalid arguments')
                            code = halpin = delay = ''
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                            continue
                    excludedHalPins = ('plasmac.torch-pulse-start', 'plasmac.ohmic-test',
                                    'plasmac.probe-test', 'plasmac.consumable-change')
                    head = _translate('HandlerClass', 'HAL Pin Error')
                    if halpin in excludedHalPins:
                        msg1 = _translate('HandlerClass', 'HAL pin')
                        msg2 = _translate('HandlerClass', 'must be pulsed')
                        msg3 = _translate('HandlerClass', 'using standard button code')
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n{msg3}\n')
                        continue
                    else:
                        try:
                            hal.get_value(halpin)
                            self.interlockRules[f'button_{bNum}'] = self.interlockRules['pulse-halpin_template'].copy()
                        except:
                            msg1 = _translate('HandlerClass', 'HAL pin')
                            msg2 = _translate('HandlerClass', 'does not exist')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}" {msg2}\n')
                            continue
                    # halPulsePins format is: button name, pulse time, button text, remaining time, button number
                    try:
                        self.halPulsePins[halpin] = [f'button_{bNum}', float(delay), bLabel, 0.0, bNum]
                    except:
                        head = _translate('HandlerClass', 'User Button Error')
                        msg1 = _translate('HandlerClass', 'Check button code for invalid seconds argument')
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                        continue
                else:
                    head = _translate('HandlerClass', 'User Button Error')
                    msg1 = _translate('HandlerClass', 'Check button code for invalid arguments')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                    continue
            elif code == 'offsets-view':
                self.ovButton = f'button_{bNum}'
                self.interlockRules[self.ovButton] = self.interlockRules['offsets-view_template'].copy()
            elif code == 'latest-file':
                self.llButton = f'button_{bNum}'
                self.interlockRules[self.llButton] = self.interlockRules['latest-file_template'].copy()
            elif code == 'user-manual':
                self.umButton = f'button_{bNum}'
                self.interlockRules[self.umButton] = self.interlockRules['user-manual_template'].copy()
                if _WEBENGINE_AVAILABLE:
                    self.w.webview.page().loadFinished.connect(self.style_user_manual)
                    self.w.webview.page().setBackgroundColor(QColor(self.backColor))
                else:
                    head = _translate('HandlerClass', 'User Button Warning')
                    msg1 = _translate('HandlerClass', 'QtWebEngine dependency missing for user button')
                    msg2 = _translate('HandlerClass', 'User Manual styling will not match GUI')
                    msg3 = _translate('HandlerClass', 'Fix using "sudo apt install python3-pyqt5.qtwebengine"')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg1} #{bNum}\n{msg2}\n{msg3}\n')
                self.w.webview.load(self.umUrl)
            elif code == 'toggle-joint':
                self.jtButton = f'button_{bNum}'
                self.interlockRules[self.jtButton] = self.interlockRules['toggle-joint_template'].copy()
            else:
                if 'dual-code' in bCode:
                    # incoming code is: "dual-code" ;; code1 ;; label1 ;; code2 ;; checked (optional = true)
                    data = bCode.split(';;')
                    if len(data) not in [4, 5]:
                        head = _translate('HandlerClass', 'User Button Error')
                        msg1 = _translate('HandlerClass', 'Check button code for invalid arguments')
                        code = halpin = delay = ''
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}\n')
                        continue
                    else:
                        if len(data) == 5 and data[4].strip().lower() == 'true':
                            self.w[f'button_{bNum}'].setCheckable(True)
                            checked = True
                        else:
                            checked = True
                        self.dualCodeButtons[bNum] = [data[1], data[2], data[3], bLabel, checked]
                        # dualCodeButtons format is: code1 ;; label1 ;; code2 ;; label2 ;; checked
                    commands = f'{data[1]}\\{data[3]}'
                else:
                    commands = bCode
                for command in commands.split('\\'):
                    command = command.strip()
                    if command and command[0].lower() in 'xyzabcgmfsto' and command.replace(' ', '')[1] in '0123456789<':
                        if f'button_{bNum}' not in self.interlockRules:
                            self.interlockRules[f'button_{bNum}'] = self.interlockRules['dual-code_template'].copy()
                    elif command and command[0] == '%':
                        cmd = command.lstrip('%').lstrip(' ').split(' ', 1)[0]
                        if cmd[-3:] == '.py':
                            reply = os.path.exists(os.path.expanduser(cmd))
                        else:
                            reply = Popen(f'which {cmd}', stdout=PIPE, stderr=PIPE, shell=True).communicate()[0]
                        if not reply:
                            head = _translate('HandlerClass', 'External Code Error')
                            msg1 = _translate('HandlerClass', 'External command')
                            msg2 = _translate('HandlerClass', 'does not exist')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{cmd}" {msg2}\n')
                        else:
                            self.interlockRules[f'button_{bNum}'] = self.interlockRules['always-on_template'].copy()
                    else:
                        head = _translate('HandlerClass', 'Code Error')
                        msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ')
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}: "{command}"\n')
                        self.remove_button(f'button_{bNum}')
                        break
        for button_name in self.tlButton:
            self.interlockRules[button_name] = self.interlockRules['toggle-laser_template'].copy()
        template_keys = [key for key in self.interlockRules.keys() if key.endswith('_template')]
        for template_key in template_keys:
            self.interlockRules.pop(template_key)

    def user_button_down(self, bNum):
        bCode = self.iniButtonCodes[bNum]
        if not bCode:
            return
        # toggle-laser is the only code that can appear anywhere in the button code
        if 'toggle-laser' in bCode.lower():
            self.laserOnPin.set(not self.laserOnPin.get())
            for command in bCode.split('\\'):
                command = command.strip()
                if command != 'toggle-laser':
                    self.user_button_command(bNum, command)
            ACTION.SET_MANUAL_MODE()
        elif bCode.lower().startswith('change-consumables') and 'e-halpin' not in bCode.lower():
            self.change_consumables(True)
        elif bCode.lower().startswith('probe-test') and 'e-halpin' not in bCode.lower():
            self.probe_test(True)
        elif bCode.lower().startswith('torch-pulse') and 'e-halpin' not in bCode.lower():
            self.torch_pulse(True)
        elif bCode.lower().startswith('ohmic-test') and 'e-halpin' not in bCode.lower():
            self.ohmic_test(True)
        elif bCode.lower().startswith('framing'):
            self.frame_job(True)
        elif bCode.lower().startswith('cut-type'):
            self.w.gcodegraphics.logger.clear()
            self.cutType ^= 1
            if self.cutType:
                self.cutTypePin.set(1)
                self.button_active(self.ctButton)
                self.cutTypeText = self.w[self.ctButton].text()
                self.w[self.ctButton].setText(_translate('HandlerClass', 'PIERCE\nONLY'))
            else:
                self.cutTypePin.set(0)
                self.button_normal(self.ctButton)
                self.w[self.ctButton].setText(self.cutTypeText)
            self.overlayProgress.setValue(0)
            if self.fileOpened:
                self.file_reload_clicked()
        elif bCode.lower().startswith('load'):
            lFile = f'{self.programPrefix}/{bCode.split("load", 1)[1].strip()}'
            self.overlayProgress.setValue(0)
            self.remove_temp_materials()
            ACTION.OPEN_PROGRAM(lFile)
        elif bCode.lower().startswith('toggle-halpin'):
            halpin = bCode.lower().split('toggle-halpin')[1].split(' ')[1].strip()
            try:
                if halpin in self.halPulsePins and self.halPulsePins[halpin][3] > 0.05:
                    self.halPulsePins[halpin][3] = 0.0
                else:
                    self.invert_pin_state(halpin)
            except Exception as err:
                head = _translate('HandlerClass', 'HAL Pin Error')
                msg0 = _translate('HandlerClass', 'Invalid code for user button')
                msg1 = _translate('HandlerClass', 'Failed to toggle HAL pin')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head,}:\n{msg0} #{bNum}\n{msg1}\n"{halpin}" {err}\n')
        elif bCode.lower().startswith('pulse-halpin'):
            head = _translate('HandlerClass', 'HAL Pin Error')
            msg1 = _translate('HandlerClass', 'Failed to pulse HAL pin')
            halpin = bCode.lower().strip().split()[1]
            # halPulsePins format is: button name, pulse time, button text, remaining time, button number
            try:
                if self.halPulsePins[halpin][3] > 0.05:
                    self.halPulsePins[halpin][3] = 0.0
                else:
                    self.w[self.halPulsePins[halpin][0]].setText(f'{self.halPulsePins[halpin][2]}')
                    self.halPulsePins[halpin][3] = self.halPulsePins[halpin][1]
                    if not self.pulseTimer.isActive():
                        self.pulseTimer.start(100)
            except:
                msg0 = _translate('HandlerClass', 'Invalid code for user button')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1} "{halpin}"\n')
        elif bCode.lower().startswith('single-cut'):
            self.single_cut()
        elif bCode.lower().startswith('manual-cut'):
            self.manual_cut()
        elif bCode.lower().startswith('offsets-view'):
            if self.w.preview_stack.currentIndex() != self.OFFSETS:
                self.w.preview_stack.setCurrentIndex(self.OFFSETS)
            else:
                self.preview_index_return(self.w.preview_stack.currentIndex())
        elif bCode.lower().startswith('latest-file'):
            try:
                if len(bCode.split()) == 2:
                    lastDir = bCode.split()[1]
                else:
                    lastDir = self.w.PREFS_.getpref('last_loaded_directory', '', str, 'BOOK_KEEPING')
                files = glob.glob(f'{lastDir}/*.ngc')
                latest = max(files, key=os.path.getctime)
                self.overlayProgress.setValue(0)
                self.remove_temp_materials()
                ACTION.OPEN_PROGRAM(latest)
            except:
                head = _translate('HandlerClass', 'File Error')
                msg0 = _translate('HandlerClass', 'Cannot open latest file from user button')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n')
        elif bCode.lower().startswith('user-manual'):
            if self.w.preview_stack.currentIndex() != self.USER_MANUAL:
                self.prevPreviewIndex = self.w.preview_stack.currentIndex()
                self.w.preview_stack.setCurrentIndex(self.USER_MANUAL)
            else:
                self.w.preview_stack.setCurrentIndex(self.prevPreviewIndex)
                self.prevPreviewIndex = self.USER_MANUAL
        elif bCode.lower().startswith('toggle-joint'):
            self.toggle_joint_mode()
        else:
            self.reloadRequired = False
            if bCode.lower().startswith('dual-code'):
                # dualCodeButtons format is: code1 ;; label1 ;; code2 ;; label2 ;; checked
                if self.w[f'button_{bNum}'].text() == self.dualCodeButtons[bNum][3]:
                    bCode = self.dualCodeButtons[bNum][0]
                    self.w[f'button_{bNum}'].setText(self.dualCodeButtons[bNum][1])
                    self.w[f'button_{bNum}'].setChecked(True)
                else:
                    bCode = self.dualCodeButtons[bNum][2]
                    self.w[f'button_{bNum}'].setText(self.dualCodeButtons[bNum][3])
                    self.w[f'button_{bNum}'].setChecked(False)
            for command in bCode.split('\\'):
                command = command.strip()
                self.user_button_command(bNum, command)
                if command[0] == "%":
                    continue
                if command.lower().replace(' ', '').startswith('g10l20') and self.fileOpened:
                    self.reloadRequired = True
            if self.reloadRequired:
                self.file_reload_clicked()
            else:
                self.w.gcodegraphics.logger.clear()
            ACTION.SET_MANUAL_MODE()

    # for G-code commands and external commands
    def user_button_command(self, bNum, command):
        if command and command[0].lower() in 'xyzabcgmfsto' and command.replace(' ', '')[1] in '0123456789<':
            if '{' in command:
                newCommand = subCommand = ''
                for char in command:
                    if char == '{':
                        subCommand = ':'
                    elif char == '}':
                        if len(subCommand.split()) > 1:
                            section, option = subCommand.replace(':', '').split(' ', 1)
                        else:
                            head = _translate('HandlerClass', 'Code Error')
                            msg0 = _translate('HandlerClass', 'Value Error in user button')
                            msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ')
                            errorCode = f'{subCommand.replace(":", "{")}' + '}'
                            msg2 = _translate('HandlerClass', 'Requires a valid section and option pair')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum} ({msg1}):\n"{errorCode}"\n{msg2}\n')
                            return
                        if self.PREFS.has_option(section, option):
                            newCommand += str(self.PREFS.get(section, option))
                        elif self.iniFile.find(section, option):
                            newCommand += self.iniFile.find(section, option)
                        else:
                            head = _translate('HandlerClass', 'Code Error')
                            msg0 = _translate('HandlerClass', 'Invalid code in user button')
                            msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ')
                            errorCode = f'{subCommand.replace(":", "{")}' + '}'
                            msg2 = _translate('HandlerClass', 'Section')
                            msg3 = _translate('HandlerClass', 'or option')
                            msg4 = _translate('HandlerClass', 'does not exist in')
                            msg5 = _translate('HandlerClass', 'Section and option pairs are case sensative')
                            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum} ({msg1}):\n"{errorCode}"\n{msg2}: "[{section}]" {msg3}: "{option}" {msg4}:\n{self.prefsFile}\n{msg5}\n')
                            return
                        subCommand = ''
                    elif subCommand.startswith(':'):
                        subCommand += char
                    else:
                        newCommand += char
                command = newCommand
            ACTION.CALL_MDI(command)
            while not STATUS.is_interp_idle():
                self.w.gcodegraphics.update()
                QApplication.processEvents()
        elif command and command[0] == '%':
            command = command.lstrip('%').lstrip()
            if command[-3:] == '.py':
                cmd = 'python3 ' + command
                Popen(cmd, stdin=None, stdout=PIPE, stderr=PIPE, shell=True)
            else:
                command += '&'
                Popen(command, stdout=PIPE, stderr=PIPE, shell=True)
        else:
            head = _translate('HandlerClass', 'Code Error')
            msg0 = _translate('HandlerClass', 'Invalid code for user button')
            msg1 = self.w[f'button_{bNum}'].text().replace('\n', ' ')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{bNum}\n{msg1}: "{command}"\n')

    def user_button_up(self, bNum):
        commands = self.iniButtonCodes[bNum]
        if not commands:
            return
        elif 'torch-pulse' in commands.lower() and 'e-halpin' not in commands.lower():
            self.torch_pulse(False)
        if 'ohmic-test' in commands.lower() and 'e-halpin' not in commands.lower():
            self.ohmic_test(False)

    def torch_enable_changed(self, state):
        if self.torchPulse:
            self.torch_pulse_abort()
        self.refresh_button_states()

    def ext_torch_enable_changed(self, state):
        if state:
            self.w.torch_enable.toggle()

    def ext_thc_enable_changed(self, state):
        if state:
            self.w.thc_enable.toggle()

    def ext_corner_lock_enable_changed(self, state):
        if state:
            self.w.cornerlock_enable.toggle()

    def ext_void_lock_enable_changed(self, state):
        if state:
            self.w.voidlock_enable.toggle()

    def ext_ignore_arc_ok_changed(self, state):
        if state:
            self.w.ignore_arc_ok.toggle()

    def ext_mesh_mode_changed(self, state):
        if state:
            self.w.mesh_enable.toggle()

    def ext_ohmic_probe_enable_changed(self, state):
        if state:
            self.w.ohmic_probe_enable.toggle()

    def ext_auto_volts_enable_changed(self, state):
        if state:
            self.w.use_auto_volts.toggle()

    def thc_auto_changed(self, state):
        if state:
            self.w.thc_delay.hide()
            self.w.thc_delay_lbl.hide()
            self.w.thc_sample_counts.show()
            self.w.thc_sample_counts_lbl.show()
            self.w.thc_sample_threshold.show()
            self.w.thc_sample_threshold_lbl.show()
        else:
            self.w.thc_delay.show()
            self.w.thc_delay_lbl.show()
            self.w.thc_sample_counts.hide()
            self.w.thc_sample_counts_lbl.hide()
            self.w.thc_sample_threshold.hide()
            self.w.thc_sample_threshold_lbl.hide()

    def consumable_change_setup(self):
        self.ccXpos = self.ccYpos = self.ccFeed = None
        X = Y = F = ''
        ccAxis = [X, Y, F]
        ccName = ['x', 'y', 'f']
        for loop in range(3):
            count = 0
            if ccName[loop] in self.ccParm:
                while 1:
                    if not self.ccParm[count]:
                        break
                    if self.ccParm[count] == ccName[loop]:
                        count += 1
                        break
                    count += 1
                while 1:
                    if count == len(self.ccParm):
                        break
                    if self.ccParm[count].isdigit() or self.ccParm[count] in '.-':
                        ccAxis[loop] += self.ccParm[count]
                    else:
                        break
                    count += 1
                if ccName[loop] == 'x' and ccAxis[loop]:
                    self.ccXpos = float(ccAxis[loop])
                elif ccName[loop] == 'y' and ccAxis[loop]:
                    self.ccYpos = float(ccAxis[loop])
                elif ccName[loop] == 'f' and ccAxis[loop]:
                    self.ccFeed = float(ccAxis[loop])
        if not self.ccFeed or self.ccFeed < 1:
            msg0 = _translate('HandlerClass', 'Invalid feed rate for consumable change')
            msg1 = _translate('HandlerClass', 'Defaulting to materials cut feed rate')
            STATUS.emit('update-machine-log', f'{msg0}, {msg1}', 'TIME')
            self.ccFeed = float(self.w.cut_feed_rate.text().replace(',', '.'))

    def ext_change_consumables(self, state):
        if self.ccButton and self.w[self.ccButton].isEnabled():
            self.change_consumables(state)

    def change_consumables(self, state):
        if hal.get_value('axis.x.eoffset-counts') or hal.get_value('axis.y.eoffset-counts'):
            hal.set_p('plasmac.consumable-change', '0')
            hal.set_p('plasmac.x-offset', '0')
            hal.set_p('plasmac.y-offset', '0')
            self.button_normal(self.ccButton)
        else:
            self.consumable_change_setup()
            if not self.ccXpos:
                self.ccXpos = STATUS.get_position()[0][0]
            if self.ccXpos < round(self.xMin, 6) + (10 * self.unitsPerMm):
                self.ccXpos = round(self.xMin, 6) + (10 * self.unitsPerMm)
            elif self.ccXpos > round(self.xMax, 6) - (10 * self.unitsPerMm):
                self.ccXpos = round(self.xMax, 6) - (10 * self.unitsPerMm)
            if not self.ccYpos:
                self.ccYpos = STATUS.get_position()[0][1]
            if self.ccYpos < round(self.yMin, 6) + (10 * self.unitsPerMm):
                self.ccYpos = round(self.yMin, 6) + (10 * self.unitsPerMm)
            elif self.ccYpos > round(self.yMax, 6) - (10 * self.unitsPerMm):
                self.ccYpos = round(self.yMax, 6) - (10 * self.unitsPerMm)
            hal.set_p('plasmac.xy-feed-rate', str(float(self.ccFeed)))
            hal.set_p('plasmac.x-offset', f'{(self.ccXpos - STATUS.get_position()[0][0]) / hal.get_value("plasmac.offset-scale"):.0f}')
            hal.set_p('plasmac.y-offset', f'{(self.ccYpos - STATUS.get_position()[0][1]) / hal.get_value("plasmac.offset-scale"):.0f}')
            hal.set_p('plasmac.consumable-change', '1')
            self.button_active(self.ccButton)
        self.refresh_button_states()

    def ext_probe_test(self, state):
        if self.ptButton and self.w[self.ptButton].isEnabled():
            self.probe_test(state)

    def probe_test(self, state):
        if state:
            if self.probeTimer.remainingTime() <= 0 and not self.offsetsActivePin.get():
                probeError, errMsg = self.bounds_check_probe(True)
                if probeError:
                    head = _translate('HandlerClass', 'Axis Limit Error')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n')
                    return
                self.probeTime = self.ptTime
                self.probeTimer.start(1000)
                self.probeTest = True
                hal.set_p('plasmac.probe-test', '1')
                self.w[self.ptButton].setText(f'{self.probeTime}')
                self.button_active(self.ptButton)
                log = _translate('HandlerClass', 'Probe test started')
                STATUS.emit('update-machine-log', log, 'TIME')
            else:
                self.probe_test_stop()
                log = _translate('HandlerClass', 'Probe test aborted')
                STATUS.emit('update-machine-log', log, 'TIME')
            self.refresh_button_states()

    def probe_test_stop(self):
        self.probeTest = False
        self.probeTimer.stop()
        self.probeTime = 0
        hal.set_p('plasmac.probe-test', '0')
        self.w[self.ptButton].setText(self.probeText)
        self.button_normal(self.ptButton)
        self.refresh_button_states()

    def probe_test_error(self, state):
        if state:
            self.probe_test_stop()
            log = _translate('HandlerClass', 'Probe test error detected')
            STATUS.emit('update-machine-log', log, 'TIME')

    def ext_torch_pulse(self, state):
        if self.tpButton and self.w[self.tpButton].isEnabled():
            self.torch_pulse(state)

    def torch_pulse(self, state):
        if state:
            self.torch_pulse_pressed()
        else:
            self.torch_pulse_released()

    def torch_pulse_pressed(self):
        # second press while active aborts
        if self.tpRemaining > 0:
            self.torch_pulse_abort()
            return
        if hal.get_value('plasmac.torch-on'):
            return
        self.tpRemaining = self.tpTime
        self.torchPulse = True
        hal.set_p('plasmac.torch-pulse-time', str(self.tpRemaining))
        hal.set_p('plasmac.torch-pulse-start', '1')
        hal.set_p('plasmac.torch-pulse-hold', '1')
        self.tpTimer.start()
        self.w[self.tpButton].setText(f'{self.tpRemaining}')
        self.button_active(self.tpButton)
        self.torch_pulse_states(False)
        log = _translate('HandlerClass', 'Torch pulse started')
        STATUS.emit('update-machine-log', log, 'TIME')

    def torch_pulse_released(self):
        hal.set_p('plasmac.torch-pulse-start', '0')
        hal.set_p('plasmac.torch-pulse-hold', '0')
        if self.tpRemaining == 0:
            self.torch_pulse_states(True)
            log = _translate('HandlerClass', 'Torch pulse hold released')
            STATUS.emit('update-machine-log', log, 'TIME')

    def torch_pulse_abort(self):
        hal.set_p('plasmac.torch-pulse-abort', '1')
        QTimer.singleShot(50, lambda: hal.set_p('plasmac.torch-pulse-abort', '0'))
        hal.set_p('plasmac.torch-pulse-start', '0')
        hal.set_p('plasmac.torch-pulse-hold', '0')
        self.tpTimer.stop()
        self.tpRemaining = 0.0
        self.torch_pulse_states(True)
        log = _translate('HandlerClass', 'Torch pulse aborted')
        STATUS.emit('update-machine-log', log, 'TIME')

    def torch_pulse_states(self, state):
        if state:
            self.w[self.tpButton].setText(self.tpText)
            self.button_normal(self.tpButton)
            self.torchPulse = False
        self.refresh_button_states()

    def ext_ohmic_test(self, state):
        if self.otButton and self.w[self.otButton].isEnabled():
            self.ohmic_test(state)

    def ohmic_test(self, state):
        self.ohmicTest = state
        hal.set_p('plasmac.ohmic-test', str(state))
        self.refresh_button_states()

    def ext_frame_job(self, state):
        if self.frButton and self.w[self.frButton].isEnabled():
            self.frame_job(state)

    def frame_job(self, state):
        if self.gcodeProps and state:
            response = False
            if self.w.laser.isEnabled():
                framingError, framePoints = self.bounds_check_framing(self.laserOffsetX, self.laserOffsetY, True)
                if framingError:
                    head = _translate('HandlerClass', 'Axis Limit Error')
                    framingError += _translate('HandlerClass', '\n\nFrame the job using the torch instead?\n')
                    response = self.dialog_show_yesno(QMessageBox.Warning, f'{head}', f'\n{framingError}')
                    if response:
                        framePoints = self.bounds_check_framing()[1]
                    else:
                        return
                else:
                    self.laserOnPin.set(1)
            else:
                framePoints = self.bounds_check_framing()[1]
            if not self.frFeed:
                feed = float(self.w.cut_feed_rate.text().replace(',', '.'))
            else:
                feed = self.frFeed
            zHeight = self.zMax - (hal.get_value('plasmac.max-offset') * self.unitsPerMm)
            if STATUS.is_on_and_idle() and STATUS.is_all_homed():
                self.framing = True
                previousMode = ''
                if self.units == 'in' and STATUS.is_metric_mode():
                    previousMode = 'G21'
                    ACTION.CALL_MDI('G20')
                elif self.units == 'mm' and not STATUS.is_metric_mode():
                    previousMode = 'G20'
                    ACTION.CALL_MDI('G21')
                ACTION.CALL_MDI_WAIT(f'G64 P{0.25 * self.unitsPerMm:0.3f}')
                if self.defaultZ:
                    ACTION.CALL_MDI(f'G53 G0 Z{zHeight:0.4f}')
                ACTION.CALL_MDI(f'G53 G0 X{framePoints[1][0]:0.2f} Y{framePoints[1][1]:0.2f}')
                ACTION.CALL_MDI(f'G53 G1 X{framePoints[2][0]:0.2f} Y{framePoints[2][1]:0.2f} F{feed:0.0f}')
                ACTION.CALL_MDI(f'G53 G1 X{framePoints[3][0]:0.2f} Y{framePoints[3][1]:0.2f}')
                ACTION.CALL_MDI(f'G53 G1 X{framePoints[4][0]:0.2f} Y{framePoints[4][1]:0.2f}')
                ACTION.CALL_MDI(f'G53 G1 X{framePoints[1][0]:0.2f} Y{framePoints[1][1]:0.2f}')
                ACTION.CALL_MDI('G0 X0 Y0')
                ACTION.CALL_MDI(previousMode)
            self.refresh_button_states()

    def single_cut(self):
        self.singleCutDialog = True
        self.refresh_button_states()
        sC = QDialog(self.w)
        sC.setWindowTitle(_translate('HandlerClass', 'Single Cut'))
        l1 = QLabel(_translate('HandlerClass', 'X LENGTH:'))
        xLength = QDoubleSpinBox()
        xLength.setAlignment(Qt.AlignRight)
        xLength.setMinimum(-9999)
        xLength.setMaximum(9999)
        xLength.setDecimals(1)
        l2 = QLabel(_translate('HandlerClass', 'Y LENGTH:'))
        yLength = QDoubleSpinBox()
        yLength.setAlignment(Qt.AlignRight)
        yLength.setMinimum(-9999)
        yLength.setMaximum(9999)
        yLength.setDecimals(1)
        l3 = QLabel('')
        buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
        buttonBox = QDialogButtonBox(buttons)
        buttonBox.accepted.connect(sC.accept)
        buttonBox.rejected.connect(sC.reject)
        buttonBox.button(QDialogButtonBox.Ok).setText(_translate('HandlerClass', 'CUT'))
        buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon())
        buttonBox.button(QDialogButtonBox.Cancel).setText(_translate('HandlerClass', 'CANCEL'))
        buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon())
        layout = QVBoxLayout()
        layout.addWidget(l1)
        layout.addWidget(xLength)
        layout.addWidget(l2)
        layout.addWidget(yLength)
        layout.addWidget(l3)
        layout.addWidget(buttonBox)
        sC.setLayout(layout)
        xLength.setValue(self.PREFS.getpref('X length', 0.0, float, 'SINGLE CUT'))
        yLength.setValue(self.PREFS.getpref('Y length', 0.0, float, 'SINGLE CUT'))
        self.vkb_show(True)
        result = sC.exec_()
        self.singleCutDialog = False
        self.refresh_button_states()
        self.vkb_hide()
        if not result or not self.cut_critical_toggle_check():
            return
        self.PREFS.putpref('X length', xLength.value(), float, 'SINGLE CUT')
        self.PREFS.putpref('Y length', yLength.value(), float, 'SINGLE CUT')
        self.oldFile = ACTION.prefilter_path if ACTION.prefilter_path else None
        self.g91 = True if 910 in STATUS.stat.gcodes else False
        xEnd = STATUS.get_position()[0][0] + xLength.value()
        yEnd = STATUS.get_position()[0][1] + yLength.value()
        newFile = f'{self.tmpPath}single_cut.ngc'
        matNum = int(self.w.materials_box.currentText().split(': ', 1)[0])
        with open(newFile, 'w') as f:
            f.write('G90\n')
            f.write(f'M190 P{matNum}\n')
            f.write('F#<_hal[plasmac.cut-feed-rate]>\n')
            f.write(f'G53 G0 X{STATUS.get_position()[0][0]:0.6f} Y{STATUS.get_position()[0][1]:0.6f}\n')
            f.write('M3 $0 S1\n')
            f.write(f'G53 G1 X{xEnd:0.6f} Y{yEnd:0.6f}\n')
            f.write('M5 $0\n')
            f.write('M2\n')
        self.single_cut_request = True
        if self.fileOpened:
            self.preSingleCutMaterial = matNum
        ACTION.OPEN_PROGRAM(newFile)

    def manual_cut(self):
        if self.manualCut:
            self.abort_pressed()
        elif STATUS.machine_is_on() and STATUS.is_all_homed() and STATUS.is_interp_idle() and self.cut_critical_toggle_check():
            probeError, errMsg = self.bounds_check_probe(True)
            if probeError:
                head = _translate('HandlerClass', 'Axis Limit Error')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{errMsg}\n')
                return
            self.manualCut = True
            self.set_mc_states(False)
            if self.mcButton:
                self.button_active(self.mcButton)
            ACTION.SET_SPINDLE_ROTATION(1, 1, 0)
            log = _translate('HandlerClass', 'Manual cut started')
            STATUS.emit('update-machine-log', log, 'TIME')

    def button_active(self, button):
        self.w[button].setStyleSheet(f'QPushButton {{ color: {self.backColor}; background: {self.fore1Color} }} \
                                     QPushButton:pressed {{ color: {self.backColor}; background: {self.fore1Color} }} \
                                        QPushButton:disabled {{ color: {self.disabledColor}}}')

    def button_normal(self, button):
        if button == 'file_open':
            self.w[button].setStyleSheet(f'QPushButton {{ color: {self.backColor}; background: {self.foreColor} }} \
                                         QPushButton:pressed {{ color: {self.foreColor}; background: {self.backColor} }} \
                                            QPushButton:disabled {{ color: {self.backColor}; background: {self.disabledColor} }}')
        else:
            self.w[button].setStyleSheet(f'QPushButton {{ color: {self.foreColor}; background: {self.backColor} }} \
                                         QPushButton:pressed {{ color: {self.backColor}; background: {self.fore1Color} }} \
                                            QPushButton:disabled {{ color: {self.disabledColor} }}')

    def button_press_timeout(self, button):
        self.w[button].setStyleSheet(f'QPushButton:pressed {{ color: {self.foreColor}; background: {self.backColor} }}')

    def button_normal_check(self, button):
        '''Returns True if the button is in the normal state (background color of the button matches the background color of the GUI)'''
        return self.w[button].palette().color(QtGui.QPalette.Background) \
            == self.w.color_backgrnd.palette().color(QPalette.Background)

    def can_enable(self, widgetName):
        rules = self.interlockRules.get(widgetName, {})
        overrideRules = rules.get('override', {})
        if overrideRules:
            for condition, value in overrideRules.items():
                if condition not in self.state:
                    LOG.error(f"'{condition}' does not exist")
                    return False
                if self.state.get(condition) == value:
                    return True
        allRules = rules.get('all', {})
        for condition, value in allRules.items():
            if condition not in self.state:
                LOG.error(f"'{condition}' does not exist")
                return False
            if self.state.get(condition) != value:
                return False
        anyRules = rules.get('any', {})
        if anyRules:
            match = False
            for condition, value in anyRules.items():
                if condition not in self.state:
                    LOG.error(f"'{condition}' does not exist")
                    return False
                if self.state.get(condition) == value:
                    match = True
                    break
            if not match:
                return False
        return True

    def sync_conditions(self):
        self.state['all_homed'] = STATUS.is_all_homed()
        self.state['consumable_change'] = hal.get_value('plasmac.consumable-change')
        self.state['consumable_changing'] = self.consChangePin.get()
        self.state['estop_cleared'] = STATUS.estop_is_clear()
        self.state['file_bounds_error'] = self.fileBoundsError
        self.state['file_opened'] = self.fileOpened
        self.state['framing'] = self.framing
        self.state['gcode_loaded'] = self.w.gcode_display.lines() > 1
        self.state['gcodestack_is_mdi'] = self.w.gcode_stack.currentIndex() == self.MDI
        self.state['interp_idle'] = STATUS.is_interp_idle()
        self.state['interp_paused'] = STATUS.is_interp_paused()
        self.state['interp_running'] = STATUS.is_interp_running()
        self.state['laser_button_state'] = self.laserButtonState == 'laser'
        self.state['machine_on'] = STATUS.machine_is_on()
        self.state['manual_cut_active'] = self.manualCut
        self.state['offsets_active'] = self.offsetsActivePin.get()
        self.state['ohmic_probe_enable'] = self.w.ohmic_probe_enable.isChecked()
        self.state['ohmic_test'] = self.ohmicTest
        self.state['plasmac_idle'] = self.plasmacStatePin.get() == self.IDLE
        self.state['previewstack_is_camera'] = self.w.preview_stack.currentIndex() == self.CAMERA
        self.state['previewstack_is_edit'] = self.w.preview_stack.currentIndex() == self.EDIT
        self.state['previewstack_is_offsets'] = self.w.preview_stack.currentIndex() == self.OFFSETS
        self.state['previewstack_is_open'] = self.w.preview_stack.currentIndex() == self.OPEN
        self.state['previewstack_is_preview'] = self.w.preview_stack.currentIndex() == self.PREVIEW
        self.state['previewstack_is_user_manual'] = self.w.preview_stack.currentIndex() == self.USER_MANUAL
        self.state['probe_bounds_error'] = self.probeBoundsError
        self.state['probe_test'] = hal.get_value('plasmac.probe-test')
        self.state['rfl_dialog'] = self.rflActive
        self.state['single_cut_dialog'] = self.singleCutDialog
        self.state['torch_enable'] = self.w.torch_enable.isChecked()
        self.state['torch_pulse'] = self.torchPulse

    def refresh_button_states(self):
        self.sync_conditions()
        for widgetName in self.interlockRules.keys():
            try:
                widget = getattr(self.w, widgetName, None)
                if widget is not None:
                    shouldEnable = self.can_enable(widgetName)
                    widget.setEnabled(shouldEnable)
            except Exception as e:
                LOG.error(f"Error updating button '{widgetName}': {e}")
        # Update tab states
        if self.tabsAlwaysEnabled.get():
            return
        for tabIndex, tabName in self.tabNames.items():
            try:
                if tabName in self.interlockRules:
                    shouldEnable = self.can_enable(tabName)
                    self.w.main_tab_widget.setTabEnabled(tabIndex, shouldEnable)
            except Exception as e:
                LOG.error(f"Error updating state for tab '{tabName}': {e}")

    def remove_button(self, widgetName):
        if widgetName in self.interlockRules:
            del self.interlockRules[widgetName]

#########################################################################################################################
# ONBOARD VIRTUAL KEYBOARD FUNCTIONS #
#########################################################################################################################

    def vkb_check(self):
        if self.w.chk_soft_keyboard.isChecked() and not os.path.isfile('/usr/bin/onboard'):
            head = _translate('HandlerClass', 'Virtual Keyboard Error')
            msg0 = _translate('HandlerClass', '"onboard" virtual keyboard is not installed')
            msg1 = _translate('HandlerClass', 'some keyboard functions are not available')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
            return
        try:
            cmd = 'gsettings get org.onboard.window.landscape width'
            self.obWidth = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip()
            cmd = 'gsettings get org.onboard.window.landscape height'
            self.obHeight = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip()
            cmd = 'gsettings get org.onboard layout'
            layout = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()[0].decode().strip()
            if '/numpad' in layout or '/keyboard' in layout:
                self.obLayout = 'compact'
            else:
                self.obLayout = layout
        except:
            self.obWidth = '700'
            self.obHeight = '300'
            self.obLayout = 'compact'

    def vkb_show(self, numpad=False):
        if self.firstRun:
            return
        if os.path.isfile('/usr/bin/onboard'):
            if self.w.chk_soft_keyboard.isChecked():
                w = '240' if numpad else '740'
                h = '240'
                l = 'numpad' if numpad else 'keyboard'
                self.vkb_setup(w, h, os.path.join(self.PATHS.IMAGEDIR, 'qtplasmac', l))
                cmd = 'dbus-send'
                cmd += ' --type=method_call'
                cmd += ' --dest=org.onboard.Onboard'
                cmd += ' /org/onboard/Onboard/Keyboard'
                cmd += ' org.onboard.Onboard.Keyboard.Show'
                Popen(cmd, stdout=PIPE, shell=True)

    def vkb_hide(self, custom=False):
        if os.path.isfile('/usr/bin/onboard') and self.obLayout:
            cmd = 'dbus-send'
            cmd += ' --type=method_call'
            cmd += ' --dest=org.onboard.Onboard'
            cmd += ' /org/onboard/Onboard/Keyboard'
            cmd += ' org.onboard.Onboard.Keyboard.Hide'
            Popen(cmd, stdout=PIPE, shell=True)
            if not custom:
                self.vkb_setup(self.obWidth, self.obHeight, self.obLayout)

    def vkb_setup(self, w, h, l):
        if os.path.isfile('/usr/bin/onboard'):
            Popen(f'gsettings set org.onboard.window.landscape width {int(w)-1}', stdout=PIPE, shell=True)
            Popen(f'gsettings set org.onboard.window.landscape height {int(h)-1}', stdout=PIPE, shell=True)
            Popen(f'gsettings set org.onboard layout {l}', stdout=PIPE, shell=True)
            Popen(f'gsettings set org.onboard.window.landscape width {int(w)}', stdout=PIPE, shell=True)
            Popen(f'gsettings set org.onboard.window.landscape height {int(h)}', stdout=PIPE, shell=True)

#########################################################################################################################
# MATERIAL HANDLING FUNCTIONS #
#########################################################################################################################

    def save_materials_clicked(self):
        matNum = self.materialChangeNumberPin.get()
        if matNum == -1:
            matNum = self.defaultMaterial
        index = self.w.materials_box.currentIndex()
        self.save_material_file(matNum, index)

    def reload_materials_clicked(self):
        self.materialUpdate = True
        index = self.w.materials_box.currentIndex()
        self.load_material_file(True)
        self.w.materials_box.setCurrentIndex(index)
        self.materialUpdate = False
        self.materialReloadPin.set(0)

    def new_material_clicked(self, repeat, value):
        head = _translate('HandlerClass', 'Add Material')
        msg1 = _translate('HandlerClass', 'Enter New Material Number')
        msgs = msg1
        btn1 = _translate('HandlerClass', 'ADD')
        btn2 = _translate('HandlerClass', 'CANCEL')
        virtkb = 4
        while 1:
            valid, matNum = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2)
            if not valid:
                return
            try:
                matNum = int(matNum)
            except:
                if not matNum:
                    msg0 = _translate('HandlerClass', 'A material number is required')
                    msgs = f'{msg0}.\n\n{msg1}'
                else:
                    msg0 = _translate('HandlerClass', 'is not a valid number')
                    msgs = f'{matNum} {msg0}.\n\n{msg1}'
                continue
            if matNum in self.materialNumList:
                msg0 = _translate('HandlerClass', 'Material')
                msg2 = _translate('HandlerClass', 'is in use')
                msgs = f'{msg0} #{matNum} {msg2}.\n\n{msg1}'
                continue
            elif matNum >= 1000000:
                msg0 = _translate('HandlerClass', 'Material numbers need to be less than 1000000')
                msgs = f'{msg0}.\n\n{msg1}'
                continue
            break
        msg1 = _translate('HandlerClass', 'Enter New Material Name')
        msgs = msg1
        virtkb = 3
        while 1:
            valid, matNam = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2)
            if not valid:
                return
            if not matNam:
                msg0 = _translate('HandlerClass', 'Material name is required')
                msgs = f'{msg0}.\n\n{msg1}'
                continue
            break
        mat = [matNum, matNam]
        mat[2:] = self.materialDict[self.materialList[self.w.materials_box.currentIndex()]][1:]
        self.write_one_material(mat)
        self.materialUpdate = True
        self.load_material_file(True)
        self.w.materials_box.setCurrentIndex(self.materialList.index(matNum))
        self.materialUpdate = False

    def delete_material_clicked(self):
        head = _translate('HandlerClass', 'Delete Material')
        msg1 = _translate('HandlerClass', 'Enter Material Number To Delete')
        btn1 = _translate('HandlerClass', 'DELETE')
        btn2 = _translate('HandlerClass', 'CANCEL')
        msgs = msg1
        virtkb = 4
        while 1:
            valid, matNum = self.dialog_input(virtkb, head, f'{msgs}:', btn1, btn2)
            if not valid:
                return
            try:
                matNum = int(matNum)
            except:
                if not matNum:
                    msg0 = _translate('HandlerClass', 'A material number is required')
                    msgs = f'{msg0}.\n\n{msg1}'
                else:
                    msg0 = _translate('HandlerClass', 'is not a valid number')
                    msgs = f'{matNum} {msg0}.\n\n{msg1}'
                continue
            if matNum == self.defaultMaterial:
                msg0 = _translate('HandlerClass', 'Default material cannot be deleted')
                msgs = f'{msg0}.\n\n{msg1}'
                continue
            elif matNum >= 1000000 and matNum in self.materialList:
                msg0 = _translate('HandlerClass', 'Temporary material')
                msg3 = _translate('HandlerClass', 'cannot be deleted')
                msgs = f'{msg0} #{matNum} {msg3}.\n\n{msg1}'
                continue
            elif matNum not in self.materialNumList:
                msg0 = _translate('HandlerClass', 'Material')
                msg3 = _translate('HandlerClass', 'does not exist')
                msgs = f'{msg0} #{matNum} {msg3}.\n\n{msg1}'
                continue
            break
        head = _translate('HandlerClass', 'Delete Material')
        msg0 = _translate('HandlerClass', 'Do you really want to delete material')
        if not self.dialog_show_yesno(QMessageBox.Question, f'{head}', f'\n{msg0} #{matNum}?\n'):
            return
        self.MATS.remove_section(f'MATERIAL_NUMBER_{matNum}')
        self.MATS.write(open(self.MATS.fn, 'w'))
        self.materialUpdate = True
        self.load_material_file(True)
        self.materialUpdate = False

    def remove_temp_materials(self):
        mats = [m for m in self.materialList if m >= 1000000]
        if mats:
            for mat in mats:
                self.materialDict.pop(mat)
                self.materialList.remove(mat)

    def selector_changed(self, index):
        if index == -1 or self.getMaterialBusy:
            return
        if self.w.material_selector.currentIndex() != self.w.materials_box.currentIndex():
            self.w.materials_box.setCurrentIndex(index)
            self.w.conv_material.setCurrentIndex(index)

    def conv_material_changed(self, index):
        if index == -1 or self.getMaterialBusy:
            return
        if self.w.conv_material.currentIndex() != self.w.materials_box.currentIndex():
            self.w.materials_box.setCurrentIndex(index)
            self.w.material_selector.setCurrentIndex(index)

    def material_changed(self, index):
        if index == -1 or self.getMaterialBusy:
            return
        if self.w.materials_box.currentText():
            if self.getMaterialBusy:
                self.materialChangePin.set(-1)
                self.autoChange = False
                return
            matNum = int(self.w.materials_box.currentText().split(': ', 1)[0])
            if matNum >= 1000000:
                self.w.save_material.setEnabled(False)
            else:
                self.w.save_material.setEnabled(True)
            if self.autoChange:
                hal.set_p('motion.digital-in-03', '0')
                if self.preSingleCutMaterial is None and self.preFileSaveMaterial is None:
                    self.change_material(matNum)
                    self.materialChangePin.set(2)
                hal.set_p('motion.digital-in-03', '1')
            else:
                self.change_material(matNum)
            self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex())
            self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex())
        self.autoChange = False
        self.overlay_update(None)

    def material_change_pin_changed(self, halpin):
        if halpin == 0:
            hal.set_p('motion.digital-in-03', '0')
        elif halpin == 3:
            hal.set_p('motion.digital-in-03', '1')
            self.materialChangePin.set(0)

    def material_change_number_pin_changed(self, halpin):
        if halpin == -1:
            halpin = self.defaultMaterial
        if self.getMaterialBusy or halpin == int(self.w.materials_box.currentText().split(': ', 1)[0]):
            return
        if self.materialChangePin.get() == 1:
            self.autoChange = True
        if not self.material_exists(halpin):
            self.autoChange = False
            return
        if self.preSingleCutMaterial is not None:
            self.materialChangeNumberPin.set(self.preSingleCutMaterial)
            self.preSingleCutMaterial = None
        elif self.preFileSaveMaterial is not None:
            self.materialChangeNumberPin.set(self.preFileSaveMaterial)
            self.preFileSaveMaterial = None
        else:
            self.w.materials_box.setCurrentIndex(self.materialList.index(halpin))

    def material_change_timeout_pin_changed(self, halpin):
        head = _translate('HandlerClass', 'Materials Error')
        if halpin:
            # should we stop or pause the program if a timeout occurs???
            matNum = int(self.w.materials_box.currentText().split(': ', 1)[0])
            msg0 = _translate('HandlerClass', 'Material change timeout occurred for material')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:{msg0} #{matNum}\n')
            self.materialChangeNumberPin.set(matNum)
            self.materialChangeTimeoutPin.set(0)
            hal.set_p('motion.digital-in-03', '0')

    def material_reload_pin_changed(self, halpin):
        if halpin:
            self.reload_materials_clicked()

    def material_temp_pin_changed(self, halpin):
        if halpin:
            mat = [halpin, f'Temporary {halpin}', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
            with open(self.tmpMaterialGcode, 'r') as f_in:
                for line in f_in:
                    if line.startswith('NAME'):
                        mat[1] = line.split('=')[1].strip()
                    if line.startswith('KERF_WIDTH'):
                        mat[2] = float(line.split('=')[1].strip())
                    elif line.startswith('PIERCE_HEIGHT'):
                        mat[3] = float(line.split('=')[1].strip())
                    elif line.startswith('PIERCE_DELAY'):
                        mat[4] = float(line.split('=')[1].strip())
                    elif line.startswith('PUDDLE_JUMP_HEIGHT'):
                        mat[5] = float(line.split('=')[1].strip())
                    elif line.startswith('PUDDLE_JUMP_DELAY'):
                        mat[6] = float(line.split('=')[1].strip())
                    elif line.startswith('CUT_HEIGHT'):
                        mat[7] = float(line.split('=')[1].strip())
                    elif line.startswith('CUT_SPEED'):
                        mat[8] = float(line.split('=')[1].strip())
                    elif line.startswith('CUT_AMPS'):
                        mat[9] = float(line.split('=')[1].strip())
                    elif line.startswith('CUT_VOLTS'):
                        mat[10] = float(line.split('=')[1].strip())
                    elif line.startswith('PAUSE_AT_END'):
                        mat[11] = float(line.split('=')[1].strip())
                    elif line.startswith('GAS_PRESSURE'):
                        mat[12] = float(line.split('=')[1].strip())
                    elif line.startswith('CUT_MODE'):
                        mat[13] = float(line.split('=')[1].strip())
                    elif line.startswith('THICKNESS'):
                        mat[14] = float(line.split('=')[1].strip())
            self.write_materials_to_dict(mat)
            if halpin not in self.materialList:
                self.materialList.append(halpin)
            exists = False
            for n in range(self.w.materials_box.count()):
                if self.w.materials_box.itemText(n) .startswith(str(halpin)):
                    self.w.materials_box.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}')
                    self.w.material_selector.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}')
                    self.w.conv_material.setItemText(n, f'{halpin:05d}: {self.materialDict[halpin][0]}')
                    exists = True
            if not exists:
                self.w.materials_box.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}')
                self.w.material_selector.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}')
                self.w.conv_material.addItem(f'{halpin:05d}: {self.materialDict[halpin][0]}')
            if halpin == int(self.w.materials_box.currentText().split(': ', 1)[0]):
                self.change_material(halpin)
            self.materialTempPin.set(0)

    def write_materials_to_dict(self, matNum):
        self.materialDict[matNum[0]] = matNum[1:]

    def display_materials(self):
        self.materialList = []
        self.w.materials_box.clear()
        self.w.material_selector.clear()
        self.w.conv_material.clear()
        for key in sorted(self.materialDict):
            self.w.materials_box.addItem(f'{key:05d}: {self.materialDict[key][0]}')
            self.w.material_selector.addItem(f'{key:05d}: {self.materialDict[key][0]}')
            self.w.conv_material.addItem(f'{key:05d}: {self.materialDict[key][0]}')
            self.materialList.append(key)

    def change_material(self, matNum):
        self.materialName = self.materialDict[matNum][0]
        self.w.kerf_width.setValue(self.materialDict[matNum][1])
        self.w.pierce_height.setValue(self.materialDict[matNum][2])
        self.w.pierce_delay.setValue(self.materialDict[matNum][3])
        self.w.puddle_jump_height.setValue(self.materialDict[matNum][4])
        self.w.puddle_jump_delay.setValue(self.materialDict[matNum][5])
        self.w.cut_height.setValue(self.materialDict[matNum][6])
        self.w.cut_feed_rate.setValue(self.materialDict[matNum][7])
        self.w.cut_amps.setValue(self.materialDict[matNum][8])
        self.w.cut_volts.setValue(self.materialDict[matNum][9])
        self.w.pause_at_end.setValue(self.materialDict[matNum][10])
        self.w.gas_pressure.setValue(self.materialDict[matNum][11])
        self.w.cut_mode.setValue(self.materialDict[matNum][12])
        self.w.material_thickness.setValue(self.materialDict[matNum][13])
        self.materialChangeNumberPin.set(matNum)

    def save_material_file(self, matNum, index):
        mat = [matNum]
        mat.append(self.w.materials_box.currentText().split(': ', 1)[1].strip())
        mat.append(self.w.kerf_width.value())
        mat.append(self.w.pierce_height.value())
        mat.append(self.w.pierce_delay.value())
        mat.append(self.w.puddle_jump_height.value())
        mat.append(self.w.puddle_jump_delay.value())
        mat.append(self.w.cut_height.value())
        mat.append(self.w.cut_feed_rate.value())
        mat.append(self.w.cut_amps.value())
        mat.append(self.w.cut_volts.value())
        mat.append(self.w.pause_at_end.value())
        mat.append(self.w.gas_pressure.value())
        mat.append(self.w.cut_mode.value())
        mat.append(self.w.material_thickness.value())
        self.write_one_material(mat)
        self.materialUpdate = True
        self.load_material_file(True)
        self.w.materials_box.setCurrentIndex(index)
        self.materialUpdate = False
        self.set_saved_material()

    def set_saved_material(self):
        mat = [int(self.w.materials_box.currentText().split(': ', 1)[0])]
        mat.append(self.materialName)
        mat.append(self.w.kerf_width.value())
        mat.append(self.w.pierce_height.value())
        mat.append(self.w.pierce_delay.value())
        mat.append(self.w.puddle_jump_height.value())
        mat.append(self.w.puddle_jump_delay.value())
        mat.append(self.w.cut_height.value())
        mat.append(self.w.cut_feed_rate.value())
        mat.append(self.w.cut_amps.value())
        mat.append(self.w.cut_volts.value())
        mat.append(self.w.pause_at_end.value())
        mat.append(self.w.gas_pressure.value())
        mat.append(self.w.cut_mode.value())
        mat.append(self.w.material_thickness.value())
        self.write_materials_to_dict(mat)

    def load_material_file(self, keepTemp=False):
        self.getMaterialBusy = True
        self.MATS = Access(self.materialFile)
        # don't remove temporary materials unless required
        if keepTemp:
            pop = [key for key in self.materialDict if key < 1000000]
            for e in pop:
                self.materialDict.pop(e)
        else:
            self.materialDict = {}
        self.materialNumList = []
        # create a basic default material if no materials exist
        if not self.MATS.sections():
            if self.units == 'mm':
                mat = [0, 'Basic default Material', 1, 3, .1, 0, 0, 1, 1000, 45, 100, 0, 0, 1, 0]
            else:
                mat = [0, 'Basic default Material', .04, .12, .1, 0, 0, .04, 40, 45, 100, 0, 0, 1, 0]
            self.write_one_material(mat)
        head = _translate('HandlerClass', 'Materials Error')
        # read all the materials into the materials dict
        for section in self.MATS.sections():
            matNum = int(section.rsplit('_', 1)[1])
            if matNum >= 1000000:
                msg0 = _translate('HandlerClass', 'Material number')
                msg1 = _translate('HandlerClass', 'is invalid')
                msg2 = _translate('HandlerClass', 'Material numbers need to be less than 1000000')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} "{matNum}" {msg1}\n{msg2}\n')
                continue
            mat = self.read_one_material(section)
            self.materialNumList.append(matNum)
            self.write_materials_to_dict(mat)
        self.display_materials()
        self.defaultMaterial = self.PREFS.getpref('Default material', self.materialNumList[0], int, 'GUI_OPTIONS')
        if not self.material_exists(self.defaultMaterial):
            self.defaultMaterial = self.materialList[0]
            self.PREFS.putpref('Default material', self.defaultMaterial, int, 'GUI_OPTIONS')
        self.change_material(self.defaultMaterial)
        self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial))
        self.w.material_selector.setCurrentIndex(self.w.materials_box.currentIndex())
        self.w.conv_material.setCurrentIndex(self.w.materials_box.currentIndex())
        self.set_default_material()
        self.getMaterialBusy = False

    def material_exists(self, matNum):
        if int(matNum) in self.materialList:
            return True
        else:
            if self.autoChange:
                self.materialChangePin.set(-1)
                self.materialChangeNumberPin.set(int(self.w.materials_box.currentText().split(': ', 1)[0]))
                head = _translate('HandlerClass', 'Materials Error')
                msg0 = _translate('HandlerClass', 'Material #')
                msg1 = _translate('HandlerClass', 'not in material list')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0} #{int(matNum)} {msg1}\n')
            return False

    def read_one_material(self, section):
        mat = [int(section.rsplit('_', 1)[1])]
        mat.append(self.MATS.getpref('NAME', 'Material', str, section))
        mat.append(self.MATS.getpref('KERF_WIDTH', 1.0 / self.unitsPerMm, float, section))
        mat.append(self.MATS.getpref('PIERCE_HEIGHT', 3.0 / self.unitsPerMm, float, section))
        mat.append(self.MATS.getpref('PIERCE_DELAY', 0.2, float, section))
        mat.append(self.MATS.getpref('PUDDLE_JUMP_HEIGHT', 0.0, float, section))
        mat.append(self.MATS.getpref('PUDDLE_JUMP_DELAY', 0.0, float, section))
        mat.append(self.MATS.getpref('CUT_HEIGHT', 1.0 / self.unitsPerMm, float, section))
        mat.append(self.MATS.getpref('CUT_SPEED', 2000.0 / self.unitsPerMm, float, section))
        mat.append(self.MATS.getpref('CUT_AMPS', 45.0, float, section))
        mat.append(self.MATS.getpref('CUT_VOLTS', 100, float, section))
        mat.append(self.MATS.getpref('PAUSE_AT_END', 0.0, float, section))
        mat.append(self.MATS.getpref('GAS_PRESSURE', 0.0, float, section))
        mat.append(self.MATS.getpref('CUT_MODE', 1.0, float, section))
        mat.append(self.MATS.getpref('THICKNESS', 0.0, float, section))
        return mat

    def write_one_material(self, mat):
        section = f'MATERIAL_NUMBER_{mat[0]}'
        self.MATS.putpref('NAME', mat[1], str, section)
        self.MATS.putpref('KERF_WIDTH', mat[2], float, section)
        self.MATS.putpref('PIERCE_HEIGHT', mat[3], float, section)
        self.MATS.putpref('PIERCE_DELAY', mat[4], float, section)
        self.MATS.putpref('PUDDLE_JUMP_HEIGHT', mat[5], float, section)
        self.MATS.putpref('PUDDLE_JUMP_DELAY', mat[6], float, section)
        self.MATS.putpref('CUT_HEIGHT', mat[7], float, section)
        self.MATS.putpref('CUT_SPEED', mat[8], float, section)
        self.MATS.putpref('CUT_AMPS', mat[9], float, section)
        self.MATS.putpref('CUT_VOLTS', mat[10], float, section)
        self.MATS.putpref('PAUSE_AT_END', mat[11], float, section)
        self.MATS.putpref('GAS_PRESSURE', mat[12], float, section)
        self.MATS.putpref('CUT_MODE', mat[13], float, section)
        self.MATS.putpref('THICKNESS', mat[14], float, section)

    def set_default_material(self):
        self.getMaterialBusy = True
        self.w.default_material.clear()
        for n in self.materialNumList:
            self.w.default_material.addItem(str(n))
        self.getMaterialBusy = False
        self.w.default_material.setCurrentIndex(self.materialList.index(self.defaultMaterial))

    def default_material_changed(self, index):
        if self.getMaterialBusy:
            return
        self.defaultMaterial = self.materialList[index]
        self.change_material(self.defaultMaterial)
        self.w.materials_box.setCurrentIndex(self.materialList.index(self.defaultMaterial))
        self.PREFS.putpref('Default material', self.defaultMaterial, int, 'GUI_OPTIONS')

#########################################################################################################################
# CAMERA AND LASER FUNCTIONS #
#########################################################################################################################

    def camera_pressed(self):
        self.w.camview.rotation = STATUS.stat.rotation_xy
        if self.w.preview_stack.currentIndex() != self.CAMERA:
            self.w.preview_stack.setCurrentIndex(self.CAMERA)
        else:
            self.preview_index_return(self.w.preview_stack.currentIndex())

    def laser_recovery_state_changed(self, value):
        hal.set_p('plasmac.laser-recovery-start', '0')

    def laser_clicked(self):
        if STATUS.is_interp_paused():
            return
        if self.laserButtonState == 'reset':
            self.laserButtonState = 'laser'
            self.button_normal('laser')
            self.refresh_button_states()
            return
        xPos = STATUS.get_position()[0][0] - self.laserOffsetX
        yPos = STATUS.get_position()[0][1] - self.laserOffsetY
        if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax:
            head = _translate('HandlerClass', 'Laser Error')
            msg0 = _translate('HandlerClass', 'Laser is outside the machine boundary')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
            return
        if self.laserButtonState == 'laser':
            self.w.laser.setText(_translate('HandlerClass', 'MARK\nEDGE'))
            self.laserButtonState = 'markedge'
            self.button_active('laser')
            self.laserOnPin.set(1)
            self.refresh_button_states()
            return
        elif self.laserButtonState == 'setorigin':
            self.button_normal('laser')
            self.laserOnPin.set(0)
        self.laserButtonState = self.sheet_align(self.laserButtonState, self.w.laser, self.laserOffsetX, self.laserOffsetY)
        self.refresh_button_states()

    def laser_pressed(self):
        if STATUS.is_interp_paused() and not self.laserRecStatePin.get():
            xPos = STATUS.get_position()[0][0] + self.laserOffsetX
            yPos = STATUS.get_position()[0][1] + self.laserOffsetY
            if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax:
                head = _translate('HandlerClass', 'Laser Error')
                msg0 = _translate('HandlerClass', 'Torch cannot move outside the machine boundary')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
                return
            hal.set_p('plasmac.laser-x-offset', f'{int(self.laserOffsetX / self.oScale)}')
            hal.set_p('plasmac.laser-y-offset', f'{int(self.laserOffsetY / self.oScale)}')
            hal.set_p('plasmac.laser-recovery-start', '1')
            hal.set_p('plasmac.cut-recovery', '1')
            self.laserOnPin.set(1)
            return
        self.laserTimer.start(750)

    def sheet_align(self, button_state, button, offsetX, offsetY):
        if button_state == 'markedge':
            zAngle = self.w.camview.rotation = 0
            ACTION.CALL_MDI_WAIT('G10 L2 P0 R0', 3)
            ACTION.SET_MANUAL_MODE()
            self.w.gcodegraphics.logger.clear()
            self.w.cam_goto.setEnabled(False)
            button.setText(_translate('HandlerClass', 'SET\nORIGIN'))
            button_state = 'setorigin'
            self.currentX = STATUS.get_position()[0][0]
            self.currentY = STATUS.get_position()[0][1]
        else:
            if button == self.w.cam_mark:
                button.setText(_translate('HandlerClass', 'MARK\nEDGE'))
                button_state = 'markedge'
            else:
                button.setText(_translate('HandlerClass', 'LASER'))
                button_state = 'laser'
            xDiff = STATUS.get_position()[0][0] - self.currentX
            yDiff = STATUS.get_position()[0][1] - self.currentY
            if xDiff and yDiff:
                zAngle = math.degrees(math.atan(yDiff / xDiff))
                if xDiff > 0:
                    zAngle += 180
                elif yDiff > 0:
                    zAngle += 360
                if abs(xDiff) < abs(yDiff):
                    zAngle -= 90
            elif xDiff:
                if xDiff > 0:
                    zAngle = 180
                else:
                    zAngle = 0
            elif yDiff:
                if yDiff > 0:
                    zAngle = 180
                else:
                    zAngle = 0
            else:
                zAngle = 0
            self.w.camview.rotation = zAngle
            ACTION.CALL_MDI_WAIT(f'G10 L20 P0 X{offsetX} Y{offsetY}')
            ACTION.CALL_MDI_WAIT(f'G10 L2 P0 R{zAngle}')
            ACTION.CALL_MDI('G0 X0 Y0')
            while not STATUS.is_interp_idle():
                self.w.gcodegraphics.update()
            if self.fileOpened:
                self.file_reload_clicked()
                self.w.gcodegraphics.logger.clear()
            self.w.cam_goto.setEnabled(True)
            ACTION.SET_MANUAL_MODE()
        return button_state

    def cam_mark_clicked(self):
        xPos = STATUS.get_position()[0][0] - self.camOffsetX
        yPos = STATUS.get_position()[0][1] - self.camOffsetY
        if xPos < self.xMin or xPos > self.xMax or yPos < self.yMin or yPos > self.yMax:
            head = _translate('HandlerClass', 'Camera Error')
            msg0 = _translate('HandlerClass', 'Camera is outside the machine boundary')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n')
            return
        self.camButtonState = self.sheet_align(self.camButtonState, self.w.cam_mark, self.camOffsetX, self.camOffsetY)

    def cam_goto_clicked(self):
        ACTION.CALL_MDI_WAIT('G0 X0 Y0')
        while not STATUS.is_interp_idle():
            self.w.gcodegraphics.update()
        ACTION.SET_MANUAL_MODE()

    def cam_zoom_plus_pressed(self):
        if self.w.camview.scale >= 5:
            return
        self.w.camview.scale += 0.1

    def cam_zoom_minus_pressed(self):
        if self.w.camview.scale <= 1:
            return
        self.w.camview.scale -= 0.1

    def cam_dia_plus_pressed(self):
        if self.w.camview.size().height() < self.w.camview.size().width():
            size = self.w.camview.size().height()
        else:
            size = self.w.camview.size().width()
        if self.w.camview.diameter >= size - 5:
            return
        self.w.camview.diameter += 2

    def cam_dia_minus_pressed(self):
        if self.w.camview.diameter <= 2:
            return
        self.w.camview.diameter -= 2

#########################################################################################################################
# STATISTICS FUNCTIONS #
#########################################################################################################################

    def statistics_show(self):
        for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']:
            self.display_hms(f'{stat}_time', hal.get_value(f'plasmac.{stat}-time'))
        self.w.cut_length.setText(f'{hal.get_value("plasmac.cut-length") / self.statsDivisor:0.2f}')
        self.w.pierce_count.setText(f'{hal.get_value("plasmac.pierce-count"):d}')
        self.statistics_load()

    def statistics_save(self, reset=False):
        self.PREFS.putpref('Cut time', f'{self.statsSaved["cut"] + hal.get_value("plasmac.cut-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Paused time', f'{self.statsSaved["paused"] + hal.get_value("plasmac.paused-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Probe time', f'{self.statsSaved["probe"] + hal.get_value("plasmac.probe-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Program run time', f'{self.statsSaved["run"] + hal.get_value("plasmac.run-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Torch on time', f'{self.statsSaved["torch"] + hal.get_value("plasmac.torch-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Rapid time', f'{self.statsSaved["rapid"] + hal.get_value("plasmac.rapid-time"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Cut length', f'{self.statsSaved["length"] + hal.get_value("plasmac.cut-length"):0.2f}', float, 'STATISTICS')
        self.PREFS.putpref('Pierce count', f'{self.statsSaved["pierce"] + hal.get_value("plasmac.pierce-count"):d}', int, 'STATISTICS')
        self.statistics_load()
        self.jobRunning = False

    def statistics_load(self):
        self.statsSaved['cut'] = self.PREFS.getpref('Cut time', 0.0, float, 'STATISTICS')
        self.statsSaved['paused'] = self.PREFS.getpref('Paused time', 0.0, float, 'STATISTICS')
        self.statsSaved['probe'] = self.PREFS.getpref('Probe time', 0.0, float, 'STATISTICS')
        self.statsSaved['run'] = self.PREFS.getpref('Program run time', 0.0, float, 'STATISTICS')
        self.statsSaved['torch'] = self.PREFS.getpref('Torch on time', 0.0, float, 'STATISTICS')
        self.statsSaved['rapid'] = self.PREFS.getpref('Rapid time', 0.0, float, 'STATISTICS')
        self.statsSaved['length'] = self.PREFS.getpref('Cut length', 0.0, float, 'STATISTICS')
        self.statsSaved['pierce'] = self.PREFS.getpref('Pierce count', 0, int, 'STATISTICS')
        for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']:
            self.display_hms(f'{stat}_time_t', self.statsSaved[f'{stat}'])
        self.w.cut_length_t.setText(f'{self.statsSaved["length"] / self.statsDivisor:0.2f}')
        self.w.pierce_count_t.setText(f'{self.statsSaved["pierce"]:d}')

    def display_hms(self, widget, time):
        m, s = divmod(time, 60)
        h, m = divmod(m, 60)
        self.w[widget].setText(f'{h:.0f}:{m:02.0f}:{s:02.0f}')

    def statistic_reset(self, stat, statT):
        if stat in ['cut_time', 'paused_time', 'probe_time', 'run_time', 'torch_time', 'rapid_time']:
            self.display_hms(f'{stat}', 0)
        elif stat == 'cut_length':
            self.w.cut_length.setText('0.00')
        elif stat == 'pierce_count':
            self.w.pierce_count.setText('0')
        self.PREFS.putpref(statT, 0.0, float, 'STATISTICS')
        self.statistics_load()

    def statistics_reset(self):
        for stat in ['cut', 'paused', 'probe', 'run', 'torch', 'rapid']:
            self.display_hms(f'{stat}_time', 0)
        self.w.cut_length.setText('0.00')
        self.w.pierce_count.setText('0')
        for stat in ['Cut time', 'Paused time', 'Probe time', 'Program run time', 'Torch on time',
                     'Rapid time', 'Cut length']:
            self.PREFS.putpref(stat, 0.0, float, 'STATISTICS')
        self.PREFS.putpref('Pierce count', 0, int, 'STATISTICS')
        self.statistics_load()

    def statistics_init(self):
        if self.unitsPerMm == 1:
            self.statsDivisor = 1000
            unit = _translate('HandlerClass', 'Metres')
        else:
            self.statsDivisor = 1
            unit = _translate('HandlerClass', 'Inches')
        msg0 = _translate('HandlerClass', 'CUT LENGTH')
        self.w.cut_length_label.setText(f'{msg0} ({unit})')
        self.statsSaved = {'cut': 0, 'length': 0, 'pierce': 0, 'paused': 0, 'probe': 0, 'rapid': 0, 'run': 0, 'torch': 0}
        self.jobRunning = False
        self.statistics_load()

#########################################################################################################################
# POWERMAX COMMUNICATIONS FUNCTIONS #
#########################################################################################################################

    def pmx485_check(self, port, periodic=False):
        try:
            import serial
            import serial.tools.list_ports as PORTS
            head = _translate('HandlerClass', 'Port Error')
            msg1 = _translate('HandlerClass', 'Powermax communications are disabled')
            ports = []
            for p in PORTS.comports():
                ports.append(p[0])
            if port in ports:
                try:
                    sPort = serial.Serial(port, 19200)
                    sPort.close()
                except Exception as err:
                    if not periodic:
                        STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{err}\n{msg1}\n')
                    return False
            else:
                if not periodic:
                    msg0 = _translate('HandlerClass', 'cannot be found')
                    STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{port} {msg0}\n{msg1}\n')
                return False
        except:
            if not periodic:
                head = _translate('HandlerClass', 'Module Error')
                msg0 = _translate('HandlerClass', 'python3-serial cannot be found')
                msg1 = _translate('HandlerClass', 'Install python3-serial or linuxcnc-dev')
            STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
            return False
        return True

    def pmx485_startup(self, port):
        self.pmx485CommsError = False
        self.w.pmx485Status = False
        self.meshMode = False
        self.w.pmx485_enable.stateChanged.connect(lambda w: self.pmx485_enable_changed(self.w.pmx485_enable.isChecked()))
        self.pmx485StatusPin.value_changed.connect(lambda w: self.pmx485_status_changed(w))
        self.pmx485ModePin.value_changed.connect(self.pmx485_mode_changed)
        self.pmx485FaultPin.value_changed.connect(lambda w: self.pmx485_fault_changed(w))
        self.pmx485ArcTimePin.value_changed.connect(lambda w: self.pmx485_arc_time_changed(w))
        self.w.gas_pressure.valueChanged.connect(self.pmx485_pressure_changed)
        self.w.mesh_enable.stateChanged.connect(lambda w: self.pmx485_mesh_enable_changed(self.w.mesh_enable.isChecked()))
        self.pmx485CommsTimer = QTimer()
        self.pmx485CommsTimer.timeout.connect(self.pmx485_timeout)
        self.pmx485RetryTimer = QTimer()
        self.pmx485RetryTimer.timeout.connect(lambda: self.pmx485_enable_changed(True))
        self.oldCutMode = self.w.cut_mode.value()
        self.pressure = self.w.gas_pressure.value()
        if self.pmx485_load(port):
            return
        self.pmx485Exists = True
        self.pmx485_setup()
        self.w.pmx485_enable.setChecked(True)

    def pmx485_load(self, port):
        head = _translate('HandlerClass', 'Comms Error')
        msg0 = _translate('HandlerClass', 'PMX485 component is not able to be loaded,')
        msg1 = _translate('HandlerClass', 'Powermax communications are not available')
        err = f'{head}:\n{msg0}\n{msg1}\n'
        count = 0
        while not hal.component_exists('pmx485'):
            if count >= 3:
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, err)
                return 1
            RUN(['halcmd', 'loadusr', '-Wn', 'pmx485', 'pmx485', f'{port}'])
            count += 1
        return 0

    def pmx485_setup(self):
        self.pins485Comp = ['pmx485.enable', 'pmx485.status', 'pmx485.fault',
                            'pmx485.mode_set', 'pmx485.mode',
                            'pmx485.current_set', 'pmx485.current', 'pmx485.current_min', 'pmx485.current_max',
                            'pmx485.pressure_set', 'pmx485.pressure', 'pmx485.pressure_min', 'pmx485.pressure_max', 'pmx485.arcTime']
        pinsSelf = ['pmx485_enable', 'pmx485_status', 'pmx485_fault',
                    'cut_mode-f', 'pmx485_mode',
                    'cut_amps-f', 'pmx485_current', 'pmx485_current_min', 'pmx485_current_max',
                    'gas_pressure-f', 'pmx485_pressure', 'pmx485_pressure_min', 'pmx485_pressure_max', 'pmx485_arc_time']
        pinType = [hal.HAL_BIT, hal.HAL_BIT, hal.HAL_FLOAT,
                   hal.HAL_FLOAT, hal.HAL_FLOAT,
                   hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT,
                   hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT, hal.HAL_FLOAT]
        for pin in self.pins485Comp:
            hal.new_sig(f'plasmac:{pin.replace("pmx485.", "pmx485_")}', pinType[self.pins485Comp.index(pin)])
            hal.connect(pin, f'plasmac:{pin.replace("pmx485.", "pmx485_")}')
            hal.connect(f'qtplasmac.{pinsSelf[self.pins485Comp.index(pin)]}', f'plasmac:{pin.replace("pmx485.", "pmx485_")}')
        self.pmx485_mesh_enable_changed(self.w.mesh_enable.isChecked())
        self.w.cut_amps.setToolTip(_translate('HandlerClass', 'Powermax cutting current'))

    def pmx485_enable_changed(self, state):
        if state:
            self.pmx485CommsError = False
            self.pmx485RetryTimer.stop()
            if self.pmx485_load(self.PREFS.getpref('Port', '', str, 'POWERMAX')):
                return
            # if pins not connected then connect them
            if not hal.pin_has_writer('pmx485.enable'):
                self.pmx485_setup()
            # ensure valid parameters before trying to connect
            if self.w.cut_mode.value() == 0 or self.w.cut_amps.value() == 0:
                head = _translate('HandlerClass', 'Materials Error')
                msg0 = _translate('HandlerClass', 'Invalid Cut Mode or Cut Amps,')
                msg1 = _translate('HandlerClass', 'cannot connect to Powermax')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}\n{msg1}\n')
                self.w.pmx485_enable.setChecked(False)
                return
            # good to go
            else:
                self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTING'))
                self.pmx485LabelState = 'CONNECT'
                self.pmx485CommsTimer.start(3000)
        else:
            self.pmx485Connected = False
            self.pmx485CommsError = False
            self.w.pmx485_label.setText('')
            self.pmx485LabelState = None
            self.w.pmx485_label.setToolTip(_translate('HandlerClass', 'Status of PMX485 communications'))
            self.pmx485CommsTimer.stop()
            self.pmx485RetryTimer.stop()

    def pmx485_mode_changed(self, widget):
        if self.pmx485Connected:
            self.w.gas_pressure.setValue(0)

    def pmx485_pressure_changed(self, pressure):
        if self.pmx485Connected:
            if pressure < self.pressure:
                if pressure < 0:
                    self.w.gas_pressure.setValue(self.gas_maximum)
                elif self.w.gas_pressure.value() < self.gas_minimum:
                    self.w.gas_pressure.setValue(0)
            elif pressure > self.pressure:
                if pressure > self.gas_maximum:
                    self.w.gas_pressure.setValue(0)
                elif pressure < self.gas_minimum:
                    self.w.gas_pressure.setValue(self.gas_minimum)
            self.pressure = self.w.gas_pressure.value()

    def pmx485_min_max_changed(self):
        if not self.pmx485Connected:
            return
        self.w.cut_amps.setMinimum(self.pmx485CurrentMinPin.get())
        self.w.cut_amps.setMaximum(self.pmx485CurrentMaxPin.get())
        self.gas_minimum = self.pmx485PressureMinPin.get()
        self.gas_maximum = self.pmx485PressureMaxPin.get()
        self.w.gas_pressure.setMinimum(-1)
        self.w.gas_pressure.setMaximum(self.gas_maximum + 1)
        if self.gas_maximum > 15:
            self.w.gas_pressure.setSuffix(' psi')
            self.w.gas_pressure.setDecimals(0)
            self.w.gas_pressure.setSingleStep(1)
        else:
            self.w.gas_pressure.setSuffix(' bar')
            self.w.gas_pressure.setDecimals(1)
            self.w.gas_pressure.setSingleStep(0.1)

    def pmx485_status_changed(self, state):
        if state != self.pmx485Connected:
            if state:
                self.pmx485CommsError = False
                self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTED'))
                self.pmx485LabelState = 'CONNECT'
                self.pmx485Connected = True
                self.pmx485_min_max_changed()
                if self.pmx485ArcTimePin.get():
                    self.pmx485_arc_time_changed(self.pmx485ArcTimePin.get())
                if self.pmx485FaultPin.get():
                    self.pmx485_fault_changed(self.pmx485FaultPin.get())
                self.pmx485CommsTimer.stop()
                self.pmx485RetryTimer.stop()
            else:
                self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR'))
                self.pmx485LabelState = None
                self.pmx485CommsError = True
                self.pmx485Connected = False
                self.pmx485RetryTimer.start(3000)

    def pmx485_arc_time_changed(self, time):
        if self.pmx485Connected:
            self.pmx485ArcTime = self.pmx485ArcTimePin.get()
            self.w.pmx_arc_time_label.setText(_translate('HandlerClass', 'ARC ON TIME'))
            self.display_hms('pmx_arc_time_t', self.pmx485ArcTime)

    def pmx485_fault_changed(self, fault):
        if self.pmx485Connected:
            faultRaw = f'{fault:04.0f}'
            self.pmx485FaultCode = f'{faultRaw[0]}-{faultRaw[1:3]}-{faultRaw[3]}'
            head = _translate('HandlerClass', 'Powermax Error')
            code = _translate('HandlerClass', 'Fault Code')
            text = _translate('HandlerClass', 'Powermax error')
            if faultRaw == '0000':
                self.w.pmx485_label.setText(_translate('HandlerClass', 'CONNECTED'))
                self.pmx485LabelState = 'CONNECT'
                self.w.pmx485_label.setToolTip(_translate('HandlerClass', 'Status of PMX485 communications'))
            elif faultRaw in self.pmx485FaultName.keys():
                if faultRaw == '0210' and self.w.pmx485.current_max.value() > 110:
                    faultMsg = self.pmx485FaultName[faultRaw][1]
                elif faultRaw == '0210':
                    faultMsg = self.pmx485FaultName[faultRaw][0]
                else:
                    faultMsg = self.pmx485FaultName[faultRaw]
                self.w.pmx485_label.setText(f'{code}: {self.pmx485FaultCode}')
                self.pmx485LabelState = None
                self.w.pmx485_label.setStatusTip(f'{text} ({self.pmx485FaultCode}) {faultMsg}')
                msg0 = _translate('HandlerClass', 'CODE')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}: {self.pmx485FaultCode}\n{faultMsg}\n')
            else:
                self.w.pmx485_label.setText(f'{code}: {faultRaw}')
                self.pmx485LabelState = None
                msg0 = _translate('HandlerClass', 'Unknown Powermax fault code')
                self.w.pmx485_label.setStatusTip(f'{msg0} ({faultRaw})')
                STATUS.emit('error', linuxcnc.OPERATOR_ERROR, f'{head}:\n{msg0}: {faultRaw}\n')

    def pmx485_mesh_enable_changed(self, state):
        if state and not self.meshMode:
            self.oldCutMode = self.w.cut_mode.value()
            self.w.cut_mode.setValue(2)
            self.w.cut_mode.setEnabled(False)
            self.meshMode = True
        elif not hal.get_value('plasmac.mesh-enable') and self.meshMode:
            self.w.cut_mode.setValue(self.oldCutMode)
            self.w.cut_mode.setEnabled(True)
            self.meshMode = False

    def pmx485_timeout(self):
        self.pmx485CommsTimer.stop()
        self.w.pmx485_label.setText(_translate('HandlerClass', 'COMMS ERROR'))
        self.pmx485LabelState = None
        self.pmx485CommsError = True
        self.pmx485Connected = False
        self.pmx485RetryTimer.start(3000)

    pmx485FaultName = {
                '0110': 'Remote controller mode invalid',
                '0111': 'Remote controller current invalid',
                '0112': 'Remote controller pressure invalid',
                '0120': 'Low input gas pressure',
                '0121': 'Output gas pressure low',
                '0122': 'Output gas pressure high',
                '0123': 'Output gas pressure unstable',
                '0130': 'AC input power unstable',
                '0199': 'Power board hardware protection',
                '0200': 'Low gas pressure',
                '0210': ('Gas flow lost while cutting', 'Excessive arc voltage'),
                '0220': 'No gas input',
                '0300': 'Torch stuck open',
                '0301': 'Torch stuck closed',
                '0320': 'End of consumable life',
                '0400': 'PFC/Boost IGBT module under temperature',
                '0401': 'PFC/Boost IGBT module over temperature',
                '0402': 'Inverter IGBT module under temperature',
                '0403': 'Inverter IGBT module over temperature',
                '0500': 'Retaining cap off',
                '0510': 'Start/trigger signal on at power up',
                '0520': 'Torch not connected',
                '0600': 'AC input voltage phase loss',
                '0601': 'AC input voltage too low',
                '0602': 'AC input voltage too high',
                '0610': 'AC input unstable',
                '0980': 'Internal communication failure',
                '0990': 'System hardware fault',
                '1000': 'Digital signal processor fault',
                '1100': 'A/D converter fault',
                '1200': 'I/O fault',
                '2000': 'A/D converter value out of range',
                '2010': 'Auxiliary switch disconnected',
                '2100': 'Inverter module temp sensor open',
                '2101': 'Inverter module temp sensor shorted',
                '2110': 'Pressure sensor is open',
                '2111': 'Pressure sensor is shorted',
                '2200': 'DSP does not recognize the torch',
                '3000': 'Bus voltage fault',
                '3100': 'Fan speed fault',
                '3101': 'Fan fault',
                '3110': 'PFC module temperature sensor open',
                '3111': 'PFC module temperature sensor shorted',
                '3112': 'PFC module temperature sensor circuit fault',
                '3200': 'Fill valve',
                '3201': 'Dump valve',
                '3202': 'Valve ID',
                '3203': 'Electronic regulator is disconnected',
                '3410': 'Drive fault',
                '3420': '5 or 24 VDC fault',
                '3421': '18 VDC fault',
                '3430': 'Inverter capacitors unbalanced',
                '3441': 'PFC over current',
                '3511': 'Inverter saturation fault',
                '3520': 'Inverter shoot-through fault',
                '3600': 'Power board fault',
                '3700': 'Internal serial communications fault',
                }

#########################################################################################################################
# CUT RECOVERY FUNCTIONS #
#########################################################################################################################

    def set_cut_recovery(self):
        if hal.get_value('plasmac.cut-recovering'):
            self.w.jog_stack.setCurrentIndex(self.CUT_RECOVERY)
            return
        self.w.jog_stack.setCurrentIndex(self.CUT_RECOVERY)
        self.cutrec_speed_changed(self.w.cut_rec_speed.value())
        hal.set_p('plasmac.cut-recovery', '0')
        self.laserOnPin.set(0)
        self.xOrig = hal.get_value('axis.x.eoffset-counts')
        self.yOrig = hal.get_value('axis.y.eoffset-counts')
        self.zOrig = hal.get_value('axis.z.eoffset-counts')
        self.oScale = hal.get_value('plasmac.offset-scale')

    def cutrec_speed_changed(self, speed):
        text = _translate('HandlerClass', 'FEED')
        if STATUS.is_metric_mode():
            self.w.cut_rec_feed.setText(f'{text}\n{self.w.cut_feed_rate.value() * speed * 0.01:0.0f}')
        else:
            self.w.cut_rec_feed.setText(f'{text}\n{self.w.cut_feed_rate.value() * speed * 0.01:0.1f}')

    def cutrec_move_changed(self, distance):
        text = _translate('HandlerClass', 'MOVE')
        self.w.cut_rec_move_label.setText(f'{text}\n{distance}')

    def cutrec_motion(self, direction):
        if self.w.cut_rec_fwd.isEnabled() and self.w.cut_rec_rev.isEnabled():
            speed = float(self.w.cut_rec_speed.value()) * 0.01 * direction
            hal.set_p('plasmac.paused-motion-speed', str(speed))
            hal.set_p('plasmac.cut-recovery', '1')

    def cutrec_move(self, state, x, y):
        if not STATUS.is_interp_paused():
            return
        if state:
            maxMove = 10
            if self.units == 'in':
                maxMove = 0.4
            laser = self.laserRecStatePin.get() > 0
            distX = hal.get_value('qtplasmac.kerf_width-f') * x
            distY = hal.get_value('qtplasmac.kerf_width-f') * y
            xNew = hal.get_value('plasmac.axis-x-position') + hal.get_value('axis.x.eoffset') - (self.laserOffsetX * laser) + distX
            yNew = hal.get_value('plasmac.axis-y-position') + hal.get_value('axis.y.eoffset') - (self.laserOffsetY * laser) + distY
            if xNew > self.xMax or xNew < self.xMin or yNew > self.yMax or yNew < self.yMin:
                return
            xTotal = hal.get_value('axis.x.eoffset') - (self.laserOffsetX * laser) + distX
            yTotal = hal.get_value('axis.y.eoffset') - (self.laserOffsetY * laser) + distY
            if xTotal > maxMove or xTotal < -maxMove or yTotal > maxMove or yTotal < -maxMove:
                return
            moveX = int(distX / self.oScale)
            moveY = int(distY / self.oScale)
            hal.set_p('plasmac.x-offset', f'{hal.get_value("plasmac.x-offset") + moveX}')
            hal.set_p('plasmac.y-offset', f'{hal.get_value("plasmac.y-offset") + moveY}')
            hal.set_p('plasmac.cut-recovery', '1')

    def cutrec_offset_changed(self, xOffset, yOffset):
        if hal.get_value('plasmac.consumable-changing'):
            return
        if xOffset > 0.001 * self.unitsPerMm or xOffset < -0.001 * self.unitsPerMm or \
           yOffset > 0.001 * self.unitsPerMm or yOffset < -0.001 * self.unitsPerMm:
            self.refresh_button_states()
        elif not self.laserRecStatePin.get():
            hal.set_p('plasmac.cut-recovery', '0')
            hal.set_p('plasmac.x-offset', '0')
            hal.set_p('plasmac.y-offset', '0')
            self.laserOnPin.set(0)

    def cutrec_cancel_pressed(self, state):
        if state:
            if hal.get_value('plasmac.cut-recovery'):
                hal.set_p('plasmac.cut-recovery', '0')
                self.laserOnPin.set(0)

#########################################################################################################################
# STYLING FUNCTIONS #
#########################################################################################################################

    def openColorDialog(self, widget):
        initColor = QColor(widget.palette().color(QPalette.Background))
        options = QColorDialog.DontUseNativeDialog
        options |= QColorDialog.ShowAlphaChannel
        color = QColorDialog.getColor(initColor, options=options)
        if color.isValid():
            widget.setStyleSheet(f'background-color: {color.name()}')
            buttons = ['foregrnd', 'foregalt', 'led', 'backgrnd', 'backgalt', 'frams', 'estop', 'disabled', 'preview']
            labels = ['Foreground', 'Highlight', 'LED', 'Background', 'Background Alt', 'Frames', 'Estop', 'Disabled', 'Preview']
            button = widget.objectName()
            label = labels[buttons.index(button.split('_')[1])]
            self.PREFS.putpref(label, color.name(), str, 'COLOR_OPTIONS')
            self.set_basic_colors()
            self.set_color_styles()
            self.preview_stack_changed()
            if self.umButton and _WEBENGINE_AVAILABLE:
                self.w.webview.page().loadFinished.connect(self.style_user_manual)
                self.w.webview.page().setBackgroundColor(QColor(self.backColor))
                self.w.webview.reload()
            self.simStyleUpdate.set(not self.simStyleUpdate.get())

    def set_basic_colors(self):
        self.foreColor = self.PREFS.getpref('Foreground', '#ffee06', str, 'COLOR_OPTIONS')
        self.fore1Color = self.PREFS.getpref('Highlight', '#ffee06', str, 'COLOR_OPTIONS')
        self.backColor = self.PREFS.getpref('Background', '#16160e', str, 'COLOR_OPTIONS')
        self.back1Color = self.PREFS.getpref('Background Alt', '#26261e', str, 'COLOR_OPTIONS')
        self.disabledColor = self.PREFS.getpref('Disabled', '#b0b0b0', str, 'COLOR_OPTIONS')
        self.estopColor = self.PREFS.getpref('Estop', '#ff0000', str, 'COLOR_OPTIONS')

    def set_color_styles(self):
        self.styleSheetFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac.qss')
        ssFile = self.PREFS.getpref('Custom style', 'None', str, 'GUI_OPTIONS')
        # if custom stylesheet try to use it
        if ssFile != 'None':
            COPY(ssFile, self.styleSheetFile)
            self.custom_stylesheet()
        # otherwise use the standard stylesheet
        else:
            self.standard_stylesheet()
        # apply the new stylesheet
        self.w.setStyleSheet('')
        with open(self.styleSheetFile, 'r') as set_style:
            self.w.setStyleSheet(set_style.read())
        # style some buttons
        buttons = ['jog_x_minus', 'jog_x_plus', 'jog_y_minus', 'jog_y_plus',
                   'jog_z_minus', 'jog_z_plus', 'jog_a_minus', 'jog_a_plus',
                   'jog_b_minus', 'jog_b_plus', 'jog_c_minus', 'jog_c_plus',
                   'cut_rec_n', 'cut_rec_ne', 'cut_rec_e', 'cut_rec_se',
                   'cut_rec_s', 'cut_rec_sw', 'cut_rec_w', 'cut_rec_nw',
                   'conv_line', 'conv_circle', 'conv_ellipse', 'conv_triangle',
                   'conv_rectangle', 'conv_polygon', 'conv_bolt', 'conv_slot',
                   'conv_star', 'conv_gusset', 'conv_sector', 'conv_block']
        conv_images = ['conv_line_point', 'conv_line_angle', 'conv_line_3p',
                       'conv_line_2pr', 'conv_arc_angle', 'conv_bolt_l',
                       'conv_circle_l', 'conv_ellipse_l', 'conv_gusset_l',
                       'conv_polygon_l', 'conv_rectangle_l', 'conv_sector_l',
                       'conv_slot_l', 'conv_star_l', 'conv_triangle_l']
        for button in buttons:
            self.color_item(button, self.foreColor, 'button')
            self.w[button].setStyleSheet(f'QPushButton {{ background: {self.backColor} }} \
                                         QPushButton:pressed {{ background: {self.backColor} }}')
        for conv_image in conv_images:
            self.color_item(conv_image, self.foreColor, 'image')
        # some gcode display/editor colors cannot use .qss file
        # gcode display current gcode line
        self.w.gcode_display.setMarkerBackgroundColor(QColor(self.back1Color))
        # gcode display active line
        self.w.gcode_display.setCaretLineBackgroundColor(QColor(self.back1Color))
        # gcode editor current gcode line
        self.w.gcode_editor.editor.setMarkerBackgroundColor(QColor(self.back1Color))
        self.w.gcode_editor.editor.setCaretForegroundColor(QColor(self.fore1Color))
        # gcode editor active line
        self.w.gcode_editor.editor.setCaretLineBackgroundColor(QColor(self.backColor))

    def standard_stylesheet(self):
        baseStyleFile = os.path.join(self.PATHS.SCREENDIR, self.PATHS.BASEPATH, 'qtplasmac.style')
        customStyleFile = os.path.join(self.PATHS.CONFIGPATH, 'qtplasmac_custom.qss')
        # Read in the base stylesheet file
        with open(baseStyleFile, 'r') as inFile:
            lines = inFile.readlines()
        # Append the base file with changes if custom stylesheet is found
        if os.path.isfile(customStyleFile):
            with open(customStyleFile, 'r') as inFile:
                lines += inFile.readlines()
        elementColorMap = {
            'backalt': self.w.color_backgalt.styleSheet().split(':')[1].strip(),
            'backgnd': self.w.color_backgrnd.styleSheet().split(':')[1].strip(),
            'e-stop': self.w.color_estop.styleSheet().split(':')[1].strip(),
            'foregnd': self.w.color_foregrnd.styleSheet().split(':')[1].strip(),
            'frames': self.w.color_frams.styleSheet().split(':')[1].strip(),
            'highlight': self.w.color_foregalt.styleSheet().split(':')[1].strip(),
            'inactive': self.w.color_disabled.styleSheet().split(':')[1].strip(),
            'l-e-d': self.w.color_led.styleSheet().split(':')[1].strip(),
            'prevu': self.w.color_preview.styleSheet().split(':')[1].strip()
            }
        with open(self.styleSheetFile, 'w') as outFile:
            for line in lines:
                for element, color in elementColorMap.items():
                    if element in line:
                        line = line.replace(element, color)
                outFile.write(line)

    def custom_stylesheet(self):
        head = _translate('HandlerClass', 'Stylesheet Error')
        try:
            # set basic colors from stylesheet header
            colors = [0, 0, 0, 0, 0]
            with open(self.styleSheetFile, 'r') as inFile:
                for line in inFile:
                    if line.startswith('color1'):
                        colors[0] += 1
                        self.foreColor = QColor(line.split('=')[1].strip()).name()
                    elif line.startswith('color2'):
                        colors[1] += 1
                        self.backColor = QColor(line.split('=')[1].strip()).name()
                    elif line.startswith('color3'):
                        colors[2] += 1
                        self.fore1Color = QColor(line.split('=')[1].strip()).name()
                    elif line.startswith('color4'):
                        colors[3] += 1
                        self.back1Color = QColor(line.split('=')[1].strip()).name()
                    elif line.startswith('color5'):
                        colors[4] += 1
                        self.disabledColor = QColor(line.split('=')[1].strip()).name()
                    if line.startswith('*'):
                        break
                if colors != [1, 1, 1, 1, 1]:
                    raise ColorError()
                # hide color buttons
                for button in ['color_foregrnd', 'color_foregrnd_lbl', 'color_foregalt',
                               'color_foregalt_lbl', 'color_backgrnd', 'color_backgrnd_lbl',
                               'color_backgalt', 'color_backgalt_lbl', 'color_frams',
                               'color_frams_lbl', 'color_estop', 'color_estop_lbl',
                               'color_disabled', 'color_disabled_lbl', 'color_preview',
                               'color_preview_lbl', 'color_led', 'color_led_lbl']:
                    self.w[button].hide()
                for button in ['camera', 'laser', self.ctButton, self.tpButton, self.ptButton, self.ccButton]:
                    if button:
                        self.button_normal(button)
        except ColorError:
            msg0 = _translate('HandlerClass', 'Invalid number of colors defined')
            msg1 = _translate('HandlerClass', 'in custom stylesheet header')
            msg2 = _translate('HandlerClass', 'Reverting to standard stylesheet')
            self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0}\n{msg1}\n\n{msg2}\n')
            self.standard_stylesheet()
        except:
            msg0 = _translate('HandlerClass', 'Cannot open custom stylesheet')
            msg1 = _translate('HandlerClass', 'Reverting to standard stylesheet')
            self.dialog_show_ok(QMessageBox.Warning, f'{head}', f'\n{msg0}\n\n{msg1}\n')
            self.standard_stylesheet()

    def color_item(self, item, color, type):
        image_path = f'{self.IMAGES}{item}.svg'
        self.image = QPixmap(image_path)
        colorChange = QPainter(self.image)
        colorChange.setCompositionMode(QPainter.CompositionMode_SourceIn)
        colorChange.fillRect(self.image.rect(), QColor(color))
        colorChange.end()
        if type == 'button':
            self.w[item].setIcon(QIcon(self.image))
        elif type == 'image':
            self[item] = QPixmap(self.image)

    def style_user_manual(self):
        # There is a brief delay between the "loadFinished" signal and the versioning site's readiness for CSS changes
        delayTime = 500 if 'qtplasmac/versions.html' in self.w.webview.url().toString() else 0
        customStyling = f"""
            setTimeout(function() {{
                var style = document.createElement('style');
                style.innerHTML = `
                    /* Apply background color to elements */
                    .caption, a, blockquote, body, figcaption, caption, code, div, div.content,
                    h1, h2, h3, h4, h5, h6, table, td, th, pre, ol, ul {{
                        background-color: {self.backColor} !important; }}

                    /* Apply foreground color to elements */
                    body, blockquote, caption, div, li, td, p {{
                        color: {self.foreColor} !important; }}

                    /* Apply highlight color to elements */
                    .caption, a, code, div.title, dt, em, figcaption, h1, h2, h3, h4, h5, h6,
                    span, strong, th, tt, ul {{
                        color: {self.fore1Color} !important; }}

                    /* Change table borders color and fix sizing */
                    table {{
                        border: 2px solid {self.foreColor} !important;
                        border-collapse: collapse !important; }}

                    /* Change table divider color and fix sizing */
                    td, th {{
                        border: 1px solid {self.foreColor} !important; }}

                    /* Remove borders from these elements, or things look odd after the other styling */
                    hr, div, div.content {{
                        border: none !important; }}

                    /* Apply highlight color to header underline */
                    h1, h2, h3, h4, h5, h6 {{
                        border-bottom: solid {self.fore1Color} !important; }}

                    /* Some images have a transparent background, this makes them visible */
                    img {{
                        background-color: white !important; }}

                    /* Apply alternate background color to highlighted sections (on section link click from TOC) */
                    :target {{
                        background: {self.back1Color} !important; }}

                    /* The following change scroll bar to match GUI styling */
                    ::-webkit-scrollbar {{
                        width: 20px;
                        height: 20px; }}

                    ::-webkit-scrollbar-thumb {{
                        background: {self.foreColor} !important;
                        border-radius: 4px;
                        min-height: 40px !important;
                        min-width: 40px !important; }}

                    ::-webkit-scrollbar-track {{
                        background: {self.back1Color} !important;
                        border-radius: 4px; }}

                    ::-webkit-scrollbar-corner {{
                        background: {self.backColor} !important; }}
                `;
                document.head.appendChild(style); }}, {delayTime});
        """
        self.w.webview.page().runJavaScript(customStyling)

#########################################################################################################################
# KEY BINDING CALLS #
#########################################################################################################################

    def key_is_valid(self, event, state):
        return self.keyboard_shortcuts() and state and not event.isAutoRepeat()

    def jog_is_valid(self, key, event):
        return self.keyboard_shortcuts() and not event.isAutoRepeat() and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w[f'jog_{key}'].isEnabled()

    def on_keycall_ESTOP(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.estopButton == 2:
            ACTION.SET_ESTOP_STATE(STATUS.estop_is_clear())

    def on_keycall_POWER(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state):
            ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on())

    def on_keycall_ABORT(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state):
            self.abort_pressed()

    def on_keycall_HOME(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and STATUS.is_on_and_idle() and self.w.home_all.isEnabled():
            if STATUS.is_all_homed():
                ACTION.SET_MACHINE_UNHOMED(-1)
            else:
                ACTION.SET_MACHINE_HOMING(-1)

    def on_keycall_RUN(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN:
            if self.w.run.isEnabled():
                self.run_clicked()
            elif self.w.pause_resume.isEnabled() and STATUS.stat.paused:
                self.pause_resume_pressed()
                ACTION.RESUME()

    def on_keycall_PAUSE(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN:
            if cntrl:
                if self.w.screen_options.desktop_notify:
                    self.w.screen_options.QTVCP_INSTANCE_._NOTICE.external_close()
                self.error_status(False)
            elif self.w.pause_resume.isEnabled() and not STATUS.stat.paused:
                ACTION.PAUSE()

    def on_keycall_OPEN(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \
           self.w.file_open.isEnabled():
            self.file_open_clicked()

    def on_keycall_LOAD(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \
           self.w.file_reload.isEnabled():
            self.file_reload_clicked()

    def on_keycall_JOINT(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN:
            self.toggle_joint_mode()

    def on_keycall_F12(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state):
            self.STYLEEDITOR.load_dialog()

    def on_keycall_F9(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and \
           not self.probeTest and not self.torchPulse and not self.framing and \
           (STATUS.is_interp_idle() or (self.manualCut and STATUS.is_interp_paused())):
            self.manual_cut()

    def on_keycall_XPOS(self, event, state, shift, cntrl):
        if self.jog_is_valid('x_plus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('x'), 1, shift)
            else:
                self.kb_jog(state, 0, 1, shift)

    def on_keycall_XNEG(self, event, state, shift, cntrl):
        if self.jog_is_valid('x_minus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('x'), -1, shift)
            else:
                self.kb_jog(state, 0, -1, shift)

    def on_keycall_YPOS(self, event, state, shift, cntrl):
        if self.jog_is_valid('y_plus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('y'), 1, shift)
            else:
                self.kb_jog(state, 1, 1, shift)

    def on_keycall_YNEG(self, event, state, shift, cntrl):
        if self.jog_is_valid('y_minus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('y'), -1, shift)
            else:
                self.kb_jog(state, 1, -1, shift)

    def on_keycall_ZPOS(self, event, state, shift, cntrl):
        if self.jog_is_valid('z_plus', event):
            if self.zPlusOverrideJog and self.w.chk_override_jog.isEnabled():
                self.w.chk_override_jog.setChecked(True)
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('z'), 1, shift)
            else:
                self.kb_jog(state, 2, 1, shift)

    def on_keycall_ZNEG(self, event, state, shift, cntrl):
        if self.jog_is_valid('z_minus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('z'), -1, shift)
            else:
                self.kb_jog(state, 2, -1, shift)

    def on_keycall_APOS(self, event, state, shift, cntrl):
        if self.jog_is_valid('a_plus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('a'), 1, shift)
            else:
                self.kb_jog(state, 3, 1, shift)

    def on_keycall_ANEG(self, event, state, shift, cntrl):
        if self.jog_is_valid('a_minus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('a'), -1, shift)
            else:
                self.kb_jog(state, 3, -1, shift)

    def on_keycall_BPOS(self, event, state, shift, cntrl):
        if self.jog_is_valid('b_plus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('b'), 1, shift)
            else:
                self.kb_jog(state, 4, 1, shift)

    def on_keycall_BNEG(self, event, state, shift, cntrl):
        if self.jog_is_valid('b_minus', event):
            if STATUS.is_joint_mode():
                self.kb_jog(state, self.coordinates.index('b'), -1, shift)
            else:
                self.kb_jog(state, 4, -1, shift)

# FIXME - keys for these jogs not defined yet
    # def on_keycall_CPOS(self, event, state, shift, cntrl):
    #     if self.jog_is_valid('c_plus', event):
    #         if STATUS.is_joint_mode():
    #             self.kb_jog(state, self.coordinates.index('c'), 1, shift)
    #         else:
    #             self.kb_jog(state, 4, 1, shift)

    # def on_keycall_CNEG(self, event, state, shift, cntrl):
    #     if self.jog_is_valid('c_minus', event):
    #         if STATUS.is_joint_mode():
    #             self.kb_jog(state, self.coordinates.index('c'), -1, shift)
    #         else:
    #             self.kb_jog(state, 4, -1, shift)

    def on_keycall_PLUS(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.jogSlow and self.w.jog_slider.isEnabled():
            return
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN:
            self.jogFast = True
        else:
            self.jogFast = False

    def on_keycall_MINUS(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.jogFast and self.w.jog_slider.isEnabled():
            return
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN:
            self.jogSlow = True
        else:
            self.jogSlow = False

    def on_keycall_NUMBER(self, event, state, shift, cntrl, number):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN:
            if shift and cntrl:
                pass
            elif shift and not cntrl:
                if number:
                    self.w.rapid_slider.setValue(10 * number)
                else:
                    self.w.rapid_slider.setValue(100)
            elif cntrl and not shift:
                if number:
                    self.w.feed_slider.setValue(10 * number)
                else:
                    self.w.feed_slider.setValue(100)
            else:
                if number and self.w.jog_slider.isEnabled():
                    if self.w.jog_slow.isChecked():
                        self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL * 0.10 * number / self.slowJogFactor)
                    else:
                        self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL * 0.10 * number)
                elif self.w.jog_slider.isEnabled():
                    if self.w.jog_slow.isChecked():
                        self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL / self.slowJogFactor)
                    else:
                        self.w.jog_slider.setValue(INFO.DEFAULT_LINEAR_JOG_VEL)

    def on_keycall_END(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.touch_xy.isEnabled():
            self.touch_xy_clicked()

    def on_keycall_DELETE(self, event, state, shift, cntrl):
        if self.keyboard_shortcuts() and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.laser.isEnabled():
            if state and not event.isAutoRepeat():
                self.extLaserButton = True
                self.laser_pressed()
            else:
                self.extLaserButton = False
                self.laser_clicked()

    def on_keycall_ALTRETURN(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and not cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.mdi_show.isEnabled():
            self.mdi_show_clicked()

    def on_keycall_RETURN(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and not cntrl and not shift and self.w.main_tab_widget.currentIndex() == self.MAIN and self.w.gcode_stack.currentIndex() == self.MDI and self.w.mdi_show.isEnabled():
            self.mdi_show_clicked()

    def on_keycall_QUOTELEFT(self, event, state, shift, cntrl):
        if self.key_is_valid(event, state) and self.w.main_tab_widget.currentIndex() == self.MAIN:
            if shift and cntrl:
                pass
            elif cntrl and not shift:
                pass
            elif shift and not cntrl:
                self.w.rapid_slider.setValue(0)
            else:
                self.w.jog_slider.setValue(0)

#########################################################################################################################
# required class boiler code #
#########################################################################################################################

    def __getitem__(self, item):
        return getattr(self, item)

    def __setitem__(self, item, value):
        return setattr(self, item, value)


#########################################################################################################################
# required handler boiler code #
#########################################################################################################################

def get_handlers(halcomp, widgets, paths):
    return [HandlerClass(halcomp, widgets, paths)]
