In some places, the API requires to attach code to an event. An event could be a menu item which is selected or a change of some status which might require some action. The API allows implementation of specific code which is called in that case. This enables us to implement the functionality behind a menu item. In this text we will refer to such functionality by the general term "callback". In general a callback is custom code that is called from the API in contrast to API code that is called from the custom code.
There are basically two ways to attach specific code to a callback:
The "Observer" class which was there prior to KLayout 0.25 has been dropped in favour of the more flexible events. It is no longer supported.
The BrowserSource (BrowserSource) class is a nice example for the Strategy pattern. It is used by the BrowserDialog class (BrowserDialog) as a kind of internal HTML server which handles URL's starting with "int:". For this, a script has to provide a class that reimplements the "get(url)" method. In the following example, a BrowserSource is created that takes an URL with an integer index number and delivers a HTML text with a link to the URL with the next index.
Here is the code. This example demonstrates how the "get" method is reimplemented to deliver the actual text.
module MyMacro include RBA class MyBrowserSource < BrowserSource def get(url) next_url = url.sub(/\d+/) { |num| (num.to_i+1).to_s } "This is #{url}. <a href='#{next_url}'>Goto next (#{next_url})</a>" end end dialog = BrowserDialog::new dialog.source = MyBrowserSource::new dialog.home = "int:0" dialog.exec endThe Python version is this:
from pya import BrowserSource, BrowserDialog class MyBrowserSource(BrowserSource): def get(self, url): next_url = "int:" + str(int(url.split(":")[1]) + 1) return f"This is {url}. <a href='{next_url}'>Goto next ({next_url})</a>>" dialog = BrowserDialog() dialog.home = "int:0" dialog.source = MyBrowserSource() dialog.exec_()
Ruby even allows reimplementation of a method without deriving a new class, because it allows defining methods per instance:
module MyMacro include RBA source = BrowserSource::new def source.get(url) next_url = url.sub(/\d+/) { |num| (num.to_i+1).to_s } "This is #{url}. <a href='#{next_url}'>Goto next (#{next_url})</a>" end dialog = BrowserDialog::new dialog.source = source dialog.home = "int:0" dialog.exec end
Events are the callback variant which is the easiest one to use. Using an event it is possible to directly attach a block of code to a callback. An event has a specific signature, i.e. the parameters it provides. The block can obtain this parameters by listing them in its argument list.
Here is a simple example that uses the parameterless "on_triggered" event of the Action class (Action). It puts a new entry into the tool bar and if it is clicked, it displays a message box:
module MyMacro include RBA action = Action::new action.on_triggered do MessageBox::info("A message", "The action was triggered", MessageBox::Ok) end action.title = "My Action" Application::instance.main_window.menu.insert_item("@toolbar.end", "my_action", action) end
The Python version is:
from pya import Action, MessageBox, Application def on_triggered(): MessageBox.info("A message", "The action was triggered", MessageBox.Ok) action = Action() action.on_triggered = on_triggered action.title = "My Action" Application.instance().main_window().menu().insert_item("@toolbar.end", "my_action", action)
Specifying a block to an event will make the event only execute that block. A more flexible way of controlling the code attached to events is available through the += and -= operators:
module MyMacro include RBA code = lambda do MessageBox::info("A message", "The action was triggered", MessageBox::Ok) end action = Action::new action.on_triggered += code ... # to remove the code from the event, use: action.on_triggered -= code # to replace all event handlers by the one given by "code": action.on_triggered = code # to clear all event handlers use: action.on_triggered.clear
Synonyms for the += operator are add and connect. The latter makes code more familiar for PyQt users. In the same way, synonyms for the -= operator are remove and disconnect.
If the Qt binding is available (see The Qt Binding), Qt signals are implemented as events. This way it's very simple to create a Qt dialog. In following example, the "textChanged" signal of QLineEdit is attached a code block which copies the text of the input field to the label below:
module MyMacro include RBA dialog = QDialog::new(Application::instance.main_window) layout = QVBoxLayout::new(dialog) input = QLineEdit::new(dialog) label = QLabel::new(dialog) layout.addWidget(input) layout.addWidget(label) # implement the textChanged signal as event: input.textChanged { |text| label.text = text } dialog.exec end
The Python version is:
from pya import QDialog, QVBoxLayout, QLineEdit, QLabel, Application dialog = QDialog(Application.instance().main_window()) layout = QVBoxLayout(dialog) input = QLineEdit(dialog) label = QLabel(dialog) layout.addWidget(input) layout.addWidget(label) def text_changed(text): label.text = text # implement the textChanged signal as event: input.textChanged = text_changed dialog.exec_()
Using the += operator on the event, multiple handlers can be added to a signal:
module MyMacro include RBA dialog = QDialog::new(Application::instance.main_window) layout = QVBoxLayout::new(dialog) input = QLineEdit::new(dialog) label1 = QLabel::new(dialog) label2 = QLabel::new(dialog) layout.addWidget(input) layout.addWidget(label1) layout.addWidget(label2) # two signal consumers: input.textChanged += lambda { |text| label1.text = text } input.textChanged += lambda { |text| label2.text = text.reverse } dialog.exec end
with the Python version:
from pya import QDialog, QVBoxLayout, QLineEdit, QLabel, Application dialog = QDialog(Application.instance().main_window()) layout = QVBoxLayout(dialog) input = QLineEdit(dialog) label1 = QLabel(dialog) label2 = QLabel(dialog) layout.addWidget(input) layout.addWidget(label1) layout.addWidget(label2) def text_changed1(text): label1.text = text def text_changed2(text): label2.text = text[::-1] # two signal consumers: input.textChanged += text_changed1 input.textChanged += text_changed2 dialog.exec_()