:lang: en
:toc:

[[cha:qtvcp-code]]
= QtVCP Handler file code snippets

Here are bits of ideas to put in the handler file.

== Preference file loading/saving

Here is how to load and save at closing time a number and some text:
You must have included a preference file option in the screenoptions
widget. The preference file path is set in the INI.

Under the 'def initialized__(self):' function add:

[source,python]
----
if self.w.PREFS_:
    # variable name (entry name, default value, type, section name)
    self.int_value = self.w.PREFS_.getpref('Integer_value', 75, int, 'CUSTOM_FORM_ENTRIES')
    self.string_value = self.w.PREFS_.getpref('String_value', 'on', str, 'CUSTOM_FORM_ENTRIES')
----

Under the 'def closing_cleanup__(self):' function add:

[source,python]
----
if self.w.PREFS_:
    # variable name (entry name, variable name, type, section name)
    self.w.PREFS_.putpref('Integer_value', self.integer_value, int, 'CUSTOM_FORM_ENTRIES')
    self.w.PREFS_.putpref('String_value', self.string_value, str, 'CUSTOM_FORM_ENTRIES')
----

== use QSettings to read/save variable

Here is how to load and save variables using Pyqt's QSettings functions:

* The file is actually saved in ~/.config/QtVcp
* You can set a Group name to help keep names organsized and unique.
* When reading a setting, if there is no entry, it will return None.
* You should set a default to cover the first time it is run.

In this example:

* We added 'or 20' and 'or 2.5' as defaults.
* The names 'MyGroupName', 'int_value', 'float_value', 'myInteger', and
  'myFloat' are user defined.

Under the 'def initialized__(self):' function add:

[source,python]
----
# set recorded columns sort settings
self.SETTINGS_.beginGroup("MyGroupName")
self.int_value = self.SETTINGS_.value('myInteger', type = int) or 20
self.float_value = self.SETTINGS_.value('myFloat', type = float) or 2.5
self.SETTINGS_.endGroup()
----

Under the 'def closing_cleanup__(self):' function add:

[source,python]
----
# save values with QSettings
self.SETTINGS_.beginGroup("MyGroupName")
self.SETTINGS_.setValue('myInteger', self.int_value)
self.SETTINGS_.setValue('myFloat', self.float_value)
self.SETTINGS_.endGroup()
----

== Add a basic style editor

Being able to edit a style on a running screen is convienant.

In the 'IMPORT SECTION':

[source,python]
----
from qtvcp.widgets.stylesheeteditor import StyleSheetEditor as SSE

STYLEEDITOR = SSE()
----

In the 'INITIALIZE SECTION', under the
`\_\_init__.(self, halcomp, widgets, paths):` function:

[source,python]
----
KEYBIND.add_call('Key_F12','on_keycall_F12')
----

Finally lets make `F12` launch it. +
In the *`KEYBINDING SECTION`* add:

[source,python]
----
def on_keycall_F12(self,event,state,shift,cntrl):
    if state:
        STYLEEDITOR.load_dialog()
----

== Request Dialog Entry

QtVCP uses STATUS messages to pop up and return information from dialogs. +
Prebuilt dialogs keep track of their last position and include options
for focus shading and sound. +
To get information back from the dialog requires using a STATUS general
message.

In the 'IMPORT SECTION' make sure there is an entry similar to this:

[source,python]
----
from qtvcp.core import Status
STATUS = Status()
----

This loads and initializes the STATUS library.

In the 'INITIALIZE SECTION'
Under the '\_\_init__.(self, halcomp, widgets, paths):' function

[source,python]
----
STATUS.connect('general',self.return_value)
----

This registers STATUS to call the function 'self.return_value' when a
general message is sent.

In the 'GENERAL FUNCTIONS SECTION'

[source,python]
----
def request_number(self):
    mess = {'NAME':'ENTRY','ID':'FORM__NUMBER', 'TITLE':'Set Tool Offset'}
    STATUS.emit('dialog-request', mess)
----

This is the function to request an entry dialog. +
NAME needs to be set to the dialogs unique launch name. +
ID needs to be set to a unique name that the function supplies +
It creates a python dict. The NAME sets which dialog to request -
'ENTRY' or 'CALCULATOR' allows entering numbers. +
The ID should be a unique key. TITLE sets the dialog title. You can also
add arbitrary data to the dict -+
the dialog will ignore them but send them back to the return code. +

In the 'CALLBACKS FROM STATUS SECTION'

[source,python]
----
# process the STATUS return message from set-tool-offset
def return_value(self, w, message):
    num = message.get('RETURN')
    id_code = bool(message.get('ID') == 'FORM__NUMBER')
    name = bool(message.get('NAME') == 'ENTRY')
    if id_code and name and num is not None:
        print('The {} number from {} was: {}'.format(name, id_code, num))
----

This catches all general messages so must check the dialog type and id
code to confirm it's our dialog. +
In this case we had requested an 'ENTRY' dialog and our unique id was
'ENTRY_NUMBER', so now we know the message is for us. +
Entry or Calculator dialogs return a float number.

== Speak a Startup Greeting

This requires the 'espeak' library installed on the system.

In the 'IMPORT SECTION' make sure there is an entry similar to this:

[source,python]
----
from qtvcp.core import Status
STATUS = Status()
----

In the 'INITIALIZE SECTION'
Under the '\_\_init__.(self, halcomp, widgets, paths):' function

[source,python]
----
STATUS.emit('play-alert','SPEAK Please remember to oil the ways.')
----

'SPEAK' is a key work, everything after it will be pronounced

== ToolBar Functions.

Toolbar buttons and submenus are added in Designer but the code to make
them do something is added in the handler file. +
In this example we assume you added a tool bar with one submenu and
three actions. +
These will be configure to create a recent file selection menu, an about
pop up dialog action, a quit program action and a user defined function
action. +
You can add submenus in designer by adding an qaction (by typing in the
toolbar column) then clicking the 'plus' icon on the right. +
This will ad a sub column that you need to type a name into. Now the
original Qaction will be a Qmenu instead. +
Now erase the Qaction you added to that Qmenu - the menu will stay as a
menu.

The objectName of the toolbar button is used to identify the button when
configuring it - descriptive names help. +
Using the action editor menu, right click and select edit. Edit the
object name, text, and button type for an appropriate action. +
In this example the submenu name must be : 'menuRecent'. The actions
must be 'actionAbout', 'actionQuit', 'actionMyFunction'

In the 'IMPORT SECTION' add:

[source,python]
----
from qtvcp.lib.toolbar_actions import ToolBarActions
----

Loads the toolbar library.

in the 'INSTANTIATE LIBRARY' Section add:

[source,python]
----
TOOLBAR = ToolBarActions()
----

In the 'SPECIAL FUNCTIONS SECTION'
Under the 'def initialized__(self):' function add:

[source,python]
----
TOOLBAR.configure_submenu(self.w.menuRecent, 'recent_submenu')
TOOLBAR.configure_action(self.w.actionAbout, 'about')
TOOLBAR.configure_action(self.w.actionQuit, 'Quit', lambda d:self.w.close())
TOOLBAR.configure_action(self.w.actionMyFunction, 'My Function', self.my_function)
----

Configures the action.

In the 'GENERAL FUNCTIONS SECTION' ADD:

[source,python]
----
def my_function(self, widget, state):
    print('My function State = ()'.format(state))
----

The function to be called if the actionMyFunction button is pressed.

== Add HAL Pins that call functions

In this way you don't need to poll the state of input pins.

In the 'IMPORT SECTION' add:

[source,python]
----
from qtvcp.core import Qhal
----

Loads the Qhal library for access to QtVCP's HAL component.

In the 'INSTANTIATE LIBRARY' Section add:

[source,python]
----
QHAL = Qhal()
----

Under the initialised__ function, make sure there is an entry similar to this:

[source,python]
----
##########################################
# Special Functions called from QtVCP
##########################################

# at this point:
# the widgets are instantiated.
# the HAL pins are built but HAL is not set ready
def initialized__(self):
    self.pin_cycle_start_in = QHAL.newpin('cycle-start-in',QHAL.HAL_BIT, QHAL.HAL_IN)
    self.pin_cycle_start_in.value_changed.connect(lambda s: self.cycleStart(s))
----

Add a function that gets called when the pin state changes.
This function assumes there is a Tab widget named 'mainTab'
that has tabs with the names 'tab_auto', 'tab_graphics',
'tab_filemanager' and 'tab_mdi'.

In this way, the cycle start
button works differently depending on what tab is showing.
This is simplified - checking state and error trapping might
be helpful.

In the 'GENERAL FUNCTIONS SECTION' add:

[source,python]
----
#####################
# general functions #
#####################

def cycleStart(self, state):
    if state:
        tab = self.w.mainTab.currentWidget()
        if  tab in( self.w.tab_auto,  self.w.tab_graphics):
            ACTION.RUN(line=0)
        elif tab == self.w.tab_files:
                self.w.filemanager.load()
        elif tab == self.w.tab_mdi:
            self.w.mditouchy.run_command()
----

== Add a special Max Velocity Slider based on percent

Some times you want to build a widget to do something not built in. +
The built in Max velocity slider acts on units per minute, here we show how to do percent: +
The STATUS command makes sure the slider adjusts if linuxcnc changes the current max velocity. +
valueChanged.connect() calls a function when the slider is moved.

In Designer add a QSlider widget called 'mvPercent'.
Then add the code to the handler file.

[source,python]
----
#############################
# SPECIAL FUNCTIONS SECTION #
#############################

def initialized__(self):
    self.w.mvPercent.setMaximum(100)
    STATUS.connect('max-velocity-override-changed', lambda w, data: self.w.mvPercent.setValue((data / INFO.MAX_TRAJ_VELOCITY)*100))
    self.w.mvPercent.valueChanged.connect(self.setMVPercentValue)

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

def setMVPercentValue(self, value):
    ACTION.SET_MAX_VELOCITY_RATE(INFO.MAX_TRAJ_VELOCITY * (value/100.0))
----

== Toggle Continuous Jog On and Off

Generally selecting continuous jogging is a momentary button, that requires you to select
the previous jog increment after. We will build a button that toggles between continuous jog and whatever
increment that was already selected.

In Designer:
- Use an action button with no action, call it 'btn_toggle_continuous'.
- Set the AbstractButton property 'checkable' true.
- Set the ActionButton properties 'incr_imperial_number' and 'incr_mm_number' to 0.

Use designer's slot editor to use the button signal 'clicked(bool)' to call form's handler function 'toggle_continuous_clicked()', see
<<cha:designer-slots,Using Designer to add slots>>.

Then add this code snippets to the handler file: +
Under the initialized__ function, make sure there is an entry similar to this:

[source,python]
----
# at this point:
# the widgets are instantiated.
# the HAL pins are built but HAL is not set ready
def initialized__(self):
    STATUS.connect('jogincrement-changed', lambda w, d, t: self.record_jog_incr(d,t))
    # set a default increment to toggle back to
    self.L_incr = 0.01
    self.L_text = "0.01in"
----

In the 'GENERAL FUNCTIONS SECTION' ADD:

[source,python]
----
#####################
# general functions #
#####################

# if it isn't continuous, record the latest jog increment
# and untoggle the continuous button
def record_jog_incr(self,d, t):
    if d != 0:
        self.L_incr = d
        self.L_text = t
        self.w.btn_toggle_continuous.safecheck(False)
----

In the 'CALLBACKS FROM FORM SECTION' ADD:

[source,python]
----
#######################
# CALLBACKS FROM FORM #
#######################

def toggle_continuous_clicked(self, state):
    if state:
        # set continuous (call the actionbutton's function)
        self.w.btn_toggle_continuous.incr_action()
    else:
        # reset previously recorded increment
        ACTION.SET_JOG_INCR(self.L_incr, self.L_text)
----

== Class Patch the file manager widget

[NOTE]
Class patching (monkey patching) is a little like black magic - so use it only if needed.

The File manager widget is designed to load a selected program in LinuxCNC.
But maybe you want to print the file name first.
We can 'class patch' the library to redirect the function call.

In the 'IMPORT SECTION' add:

[source,python]
----
from qtvcp.widgets.file_manager import FileManager as FM
----

Here we are going to keep a reference to the original function, so we can still call it.
Then we redirect the class to call our custom function in the handler file instead.

[source,python]
----
##########################################
# Special Functions called from QtVCP
##########################################

# For changing functions in widgets we can 'class patch'.
# class patching must be done before the class is instantiated.
def class_patch__(self):
    self.old_load = FM.load # keep a reference of the old function
    FM.load = self.our_load # redirect function to our handle file function
----

Ok Now we write a custom function to replace the original.
This function must have the same signature as the original function.
In this example we are still going to call the original function by using the
reference to it we recorded earlier. It requires the first argument to be the widget instance
which in this case is self.w.filemanager (the name given in the designer editor).

[source,python]
----
#####################
# GENERAL FUNCTIONS #
#####################

def our_load(self,fname):
    print(fname)
    self.old_load(self.w.filemanager,fname)
----

Now our custom function will print the file path to the terminal before loading the file.
Obviously boring but shows the principle.

There is another slightly different way to do this that can have advantages.
You can store the reference to the original function in the original class.
the trick here is to make sure the function name you use to store it, is not already
used in the class. 'super__' added to the function name would be a good choice.
We won't use that in built in QtVCP widgets.

[source,python]
----
##########################################
# Special Functions called from QtVCP
##########################################

# For changing functions in widgets we can 'class patch'.
# class patching must be done before the class is instantiated.
def class_patch__(self):
    FM.super__load = FM.load # keep a reference of the old function in the original class
    FM.load = self.our_load # redirect function to our handle file function

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

def our_load(self,fname):
    print(fname)
    self.w.filemanager.super__load(fname)
----

== Adding widgets Programmatically

In some situation it is only possible to add widgets with python code rather
then using the Designer editor.
When adding QtVCP widgets programmatically, sometimes there are extra steps to
be taken.
Here we are going to add a spindle speed indicator bar and up-to-speed LED to a
tab widget corner.
Qt Designer does not support adding corner widgets to tabs but PyQt does.
This is a cut down example from Qtaxis screen's handler file.

First we must import the libraries we need.
Often these libraries are already imported in the handler file:
- QtWidgets gives us access to the QProgress bar.
- QColor is for the LED color.
- StateLED is the QtVCP library used to create the spindle-at-speed LED.
- Status is used to catch LinuxCNC status information.
- Info gives us information about the machine configuration.

[source,python]
----
############################
# **** IMPORT SECTION **** #
############################

from PyQt5 import QtWidgets
from PyQt5.QtGui import QColor
from qtvcp.widgets.state_led import StateLED as LED
from qtvcp.core import Status, Info
----

STATUS and INFO are initialized outside the handler class so as to be a global
reference (no self. in front).

[source,python]
----
##########################################
# **** instantiate libraries section **** #
###########################################

STATUS = Status()
INFO = Info()
----

For the spindle speed indicator we need to know the current spindle speed:
We register with STATUS to catch the 'actual-spindle-speed-changed' signal to
call a function named: 'self.update_spindle()'.

[source,python]
----
########################
# **** INITIALIZE **** #
########################
# widgets allows access to  widgets from the QtVCP files
# at this point the widgets and hal pins are not instantiated
def __init__(self, halcomp,widgets,paths):
    self.hal = halcomp
    self.w = widgets
    self.PATHS = paths

    STATUS.connect('actual-spindle-speed-changed', lambda w,speed: self.update_spindle(speed))
----

We need to make sure the Designer widgets are already built before we try to
add to them. +
We add a function call 'self.make_corner_widgets()' to build our extra widgets
at the right time. +

[source,python]
----
##########################################
# Special Functions called from QTSCREEN
##########################################

# at this point:
# the widgets are instantiated.
# the HAL pins are built but HAL is not set ready
def initialized__(self):
    self.make_corner_widgets()
----

Ok let's code the function to build the widgets and add them in the tab
widget. +
We are assuming there is a tab widget built with Designer called 'rightTab'.

'self.w.led = LED()' - this initializes the basic StateLed widget and uses
self.w.led as the reference from then on. +
'self.w.led.setProperty("is_spindle_at_speed_status",True)' - since the state
LED can be used for many indications +
we must set the property that designates it as a  spindle-at-speed LED. +
'self.w.led.setProperty("color",QColor(0,255,0,255))' this sets it as green
when on. +
'self.w.led.hal_init(HAL_NAME = "spindle_is_at_speed")' - this is the extra
function call required with some QtVCP widgets. +
If HAL_NAME is omitted it will use the widget objectName if there is one. +
It gives the special widgets reference to:

* self.HAL_GCOMP_ - The HAL component wrapped in QtVCP's core QComponent
* self.HAL_NAME_ -The HAL widget name
* self.QT_OBJECT_ -the  actual object
* self.QTVCP_INSTANCE_- The window object
* self.PATHS_ -the path library
* self.PREFS_ -the preference object.

'self.w.rpm_bar = QtWidgets.QProgressBar()' - initialize a PyQt5 QProgress
bar. +
'self.w.rpm_bar.setRange(0, INFO.MAX_SPINDLE_SPEED)' - set the max range of the
Progress bar to the max specified in the INI.

Since you can only add one widget to the tab corner and we have two we want
there, we must add the two into a container. +
We create a QWidget and add a QHBoxLayout to the QWidget. +
The we add our QProgress bar and LED to the layout.

'self.w.rightTab.setCornerWidget(w)' - finally we add the QWidget (with our
QProgress bar and LED in it) to the tab widget's corner.

[source,python]
----
#####################
# general functions #
#####################

def make_corner_widgets(self):
    # make a spindle-at-speed green LED
    self.w.led = LED()
    self.w.led.setProperty('is_spindle_at_speed_status',True)
    self.w.led.setProperty('color',QColor(0,255,0,255))
    self.w.led.hal_init(HAL_NAME = 'spindle_is_at_speed')

    # make a spindle speed bar
    self.w.rpm_bar = QtWidgets.QProgressBar()
    self.w.rpm_bar.setRange(0, INFO.MAX_SPINDLE_SPEED)

    # container
    w = QtWidgets.QWidget()
    w.setContentsMargins(0,0,0,6)
    w.setMinimumHeight(40)

    # layout
    hbox = QtWidgets.QHBoxLayout()
    hbox.addWidget(self.w.rpm_bar)
    hbox.addWidget(self.w.led)
    w.setLayout(hbox)

    # add the container to the corner of the right tab widget
    self.w.rightTab.setCornerWidget(w)
----

Now we build the function to actually update out QProgressBar when STATUS
updates the spindle speed. +
'self.w.rpm_bar.setInvertedAppearance()' - In this case we chose to display
left-to-right or right-to-left depending if we are turning clockwise or
anticlockwise. +
'self.w.rpm_bar.setFormat()' - This formats the writing in the bar. +
'self.w.rpm_bar.setValue()' - This sets the length of the colored bar.

[source,python]
----
########################
# callbacks from STATUS #
########################
def update_spindle(self, data):
    self.w.rpm_bar.setInvertedAppearance(bool(data<0))
    self.w.rpm_bar.setFormat('{0:d} RPM'.format(int(data)))
    self.w.rpm_bar.setValue(abs(data))
----

== Update/Read Objects Periodically

Sometimes you need to update a widget or read a value regularly that
isn't covered by normal libraries. +
Here we update an LED based on a watched HAL pin every 100ms. +
We assume there is an LED named 'led' in the designer .ui file.

In the 'IMPORT SECTION' add:

[source,python]
----
from qtvcp.core import Qhal
----

Loads the Qhal library for access to QtVCP's hal component.

in the 'INSTANTIATE LIBRARY' Section add:

[source,python]
----
QHAL = Qhal()
----

Now add/modify these sections to include code that is similar to this:

[source,python]
----
########################
# **** INITIALIZE **** #
########################
# widgets allows access to  widgets from the QtVCP files
# at this point the widgets and hal pins are not instantiated
def __init__(self, halcomp,widgets,paths):
    self.hal = halcomp
    self.w = widgets
    self.PATHS = paths

    # register a function to be called at CYCLE_TIME period (usually every 100ms)
    STATUS.connect('periodic', lambda w: self.update_periodic())

#####################
# general functions #
#####################
def update_periodic(self):
    data = QHAL.getvalue('spindle.0.is-oriented')
    self.w.led.setState(data)
----

== external control with ZMQ messaging reading

Sometimes you want to control the screen with a separate program. +
QtVCP can automatically set up ZMQ messaging to send and/or receive remote
messages. +
It uses ZMQ's publish/subscribe pattern of messages. +
As always consider security before letting programs interface though
messaging. +
In the screenoptions widget, you can select the property
'use_receive_zmq_option' +
You could also set this property directly in the handler file (as in
this sample). +
We assume the screenoption widget is called 'screen_options' in designer:

[source,python]
----
########################
# **** INITIALIZE **** #
########################
# widgets allows access to  widgets from the QtVCP files
# at this point the widgets and hal pins are not instantiated
def __init__(self, halcomp,widgets,paths):
    # directly select ZMQ message receiving
    self.w.screen_options.setProperty('use_receive_zmq_option',True)
----

This allows an external program to call functions in the handler file. +
Let's add a specific function for testing. +
You will need to run linuxcnc from a terminal to see the printed text.

[source,python]
----
    #####################
    # general functions #
    #####################
    def test_zmq_function(self, arg1, arg2):
        print('zmq test function called:',arg1, arg2)
----

Here is a sample program to call a function. +
It alternates between two data sets every second. +
Run this in a separate terminal from linuxcnc to see the sent messages.

[source,python]
----
#!/usr/bin/env python3
from time import sleep

import zmq
import json

context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://127.0.0.1:5690")
topic = b'QtVCP'

# prebuild message 1
# makes a dict of function to call plus any arguments
x = {
  "FUNCTION": "test_zmq_function",
  "ARGS": [True,200]
}
# convert to json object
m1 = json.dumps(x)

# prebuild message 2
x = {
  "FUNCTION": "test_zmq_function",
  "ARGS": [False,0],
}
# convert to json object
m2 = json.dumps(x)

if __name__ == '__main__':
    while True:
        print('send message 1')
        socket.send_multipart([topic, bytes((m1).encode('utf-8'))])
        sleep(ms(1000))

        print('send message 2')
        socket.send_multipart([topic, bytes((m2).encode('utf-8'))])
        sleep(ms(1000))
----

Note the line 'x = {"FUNCTION": "test_zmq_function", "ARGS": [True,200]}'
sets the function to call and the arguments to send to that function. +
you will need to know the signature of the function you wish to call. +
Also note that the message is converted to a json object. +
This is because ZMQ sends byte messages not python objects. +
json converts python to bytes and will be converted back when received.

== external control with ZMQ messaging writing

You also my want to communicate with a separate program from the screen. +
QtVCP can automatically set up ZMQ messaging to send and/or receive remote
messages. +
It uses ZMQ's publish/subscribe pattern of messages. +
As always consider security before letting programs interface though
messaging. +
In the screenoptions widget, you can select the property
'use_send_zmq_message' +
You could also set this property directly in the handler file (as in this
sample). +
We assume the screenoption widget is called 'screen_options' in designer: +

[source,python]
----
########################
# **** INITIALIZE **** #
########################
# widgets allows access to  widgets from the QtVCP files
# at this point the widgets and hal pins are not instantiated
def __init__(self, halcomp,widgets,paths):
    # directly select ZMQ message sending
    self.w.screen_options.setProperty('use_send_zmq_option',True)
----

This allows sending messages to a separate program. +
The message sent will depend on what the external program is expecting. +
Let's add a specific function for testing. +
You will need to run linuxcnc from a terminal to see the printed text. +
We assume the screenoption widget is called 'screen_options' in designer: +
You need to add something to call this function, such as a button click. +

[source,python]
----
#####################
# general functions #
#####################
def send_zmq_message(self):
    # This could be any python object json can convert
    message = {"name": "John", "age": 30}
    self.w.screen_options.send_zmq_message(message)
----

Here is a sample program that will receive the message and print it to the
terminal. +

[source,python]
----
import zmq
import json

# ZeroMQ Context
context = zmq.Context()

# Define the socket using the "Context"
sock = context.socket(zmq.SUB)

# Define subscription and messages with topic to accept.
topic = "" # all topics
sock.setsockopt(zmq.SUBSCRIBE, topic)
sock.connect("tcp://127.0.0.1:5690")

while True:
    topic, message = sock.recv_multipart()
    print('{} sent message:{}'.format(topic,json.loads(message)))

----

== Sending messages to status bar or desktop notify dialogs
There several ways to report information to the user. +
A statusbar is used for short information to show the user. +
+
Not all screens have a statusbar. This is how you would use it. +
'timeout' is seconds. 'statusbar' is the designer set name. +

[source,python]
----
self.w.statusbar.showMessage(message, timeout * 1000)
----

You can use the STATUS Library to send a message to the notify Library +
if it is enabled (usually set in screen options widget). +
This will send the message to the statusbar and the desktop notify dialog. +
The messages are also recorded until the user erases them using controls. +
The users can recall any recorded messages. +
 +
There are several options. TEMPARAY will show the message for a short
time only. +
----
STATUS.TEMPARARY_MESSAGE
STATUS.OPERATOR_ERROR
STATUS.OPERATOR_TEXT
STATUS.NML_ERROR
STATUS.NML_TEXT
----

Here is an example of sending an operator message:
[source,python]
----
STATUS.emit('error', STATUS.OPERATOR_ERROR, 'message')
----

You can send messages thru linuxcnc's operator message functions. +
This are usually caught by the notify system, so are equal to above. +
They would be printed to the terminal as well. +

[source,python]
----
ACTION.SET_DISPLAY_MESSAGE('MESSAGE')
ACTION.SET_ERROR_MESSAGE('MESSAGE')
----

== Catch Focus Changes

Focus is used to direct user action such as keyboard entry to the proper
widget. +
We may want to know what widget currently has focus: +

[source,python]
----
fwidget = QtWidgets.QApplication.focusWidget()
if fwidget is not None:
    print("focus widget class: {} name: {} ".format( fwidget, fwidget.objectName()))
----

Or we may want to know what widget has focus right when focus changes:

[source,python]
----
# at this point:
# the widgets are instantiated.
# the HAL pins are built but HAL is not set ready
def initialized__(self):
    QtWidgets.QApplication.instance().event_filter.focusIn.connect(self.focusInChanged)

#####################
# general functions #
#####################

def focusInChanged(self, widget):
    if isinstance(widget.parent(),type(self.w.gcode_editor.editor)):
        print('G-code Editor')
    elif isinstance(widget,type(self.w.gcodegraphics)):
        print('G-code Display')
    elif isinstance(widget.parent(),type(self.w.mdihistory) ):
        print('MDI History')
----

Notice we sometimes compare to 'widget', sometimes 'widget.parent()'. +
This is because some QtVCP widgets are built from multiple sub-widgets
and the sub-widgets actually get the focus; so we need to check the parent
of those sub-widgets. +
Other times the main widget is what gets the focus; ie G-code display
widget can be set to accept focus and in that case there are no sub-widgets
in it so comparing to the widget.parent() would get you the container
that holds the G-code widget.

// vim: set syntax=asciidoc:
