Message combinators¶
Shorter ways to build the message dataclasses. See Messages in the guide for the objects these return.
Unstable
py2tosc.ui encodes opinions about how bindings are best described, rather than binding to the file format. It is kept out of the core namespace so those opinions can change without dragging the rest of the library's version with them, and it may change shape before 1.0.
Sources¶
The same four constructors describe an OSC argument, a MIDI slot and either end of a local binding.
value ¶
value(
key: str = "x",
*,
conversion: Conversion | str = Conversion.FLOAT,
scale: tuple[float, float] = (0.0, 1.0),
) -> Partial
A partial reading one of the control's live values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The value to read, usually |
'x'
|
conversion
|
Conversion | str
|
The type the value is converted to before sending. |
FLOAT
|
scale
|
tuple[float, float]
|
Low and high ends of the output range. |
(0.0, 1.0)
|
Returns:
| Type | Description |
|---|---|
Partial
|
A |
Source code in src/py2tosc/ui.py
const ¶
const(
text: str,
*,
conversion: Conversion | str = Conversion.STRING,
scale: tuple[float, float] = (0.0, 1.0),
) -> Partial
A partial carrying fixed text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The text to send. |
required |
conversion
|
Conversion | str
|
The type the text is converted to before sending. |
STRING
|
scale
|
tuple[float, float]
|
Low and high ends of the output range. Irrelevant to an OSC address, where the text is the whole of the content, but a MIDI slot takes its fixed byte from this range instead. |
(0.0, 1.0)
|
Returns:
| Type | Description |
|---|---|
Partial
|
A |
Source code in src/py2tosc/ui.py
prop ¶
prop(
key: str,
*,
conversion: Conversion | str = Conversion.STRING,
scale: tuple[float, float] = (0.0, 1.0),
) -> Partial
A partial reading one of the control's properties.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The property to read. Dotted lookups reach upwards, as in
|
required |
conversion
|
Conversion | str
|
The type the property is converted to before sending. |
STRING
|
scale
|
tuple[float, float]
|
Low and high ends of the output range. |
(0.0, 1.0)
|
Returns:
| Type | Description |
|---|---|
Partial
|
A |
Source code in src/py2tosc/ui.py
index ¶
index(
*,
conversion: Conversion | str = Conversion.INTEGER,
scale: tuple[float, float] = (1.0, 2.0),
) -> Partial
A partial carrying the control's position within its parent.
The defaults are the only combination the corpus contains: every one of the
147 INDEX partials in it converts to INTEGER over a 1-2 range.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conversion
|
Conversion | str
|
The type the index is converted to before sending. |
INTEGER
|
scale
|
tuple[float, float]
|
Low and high ends of the output range. |
(1.0, 2.0)
|
Returns:
| Type | Description |
|---|---|
Partial
|
An |
Source code in src/py2tosc/ui.py
Addresses¶
path ¶
Expand an address into the partials that build it.
Braces mark a property lookup, mirroring f-strings, and {#} marks the
control's index:
Adjacent literal text coalesces into a single CONSTANT partial, which is
how TouchOSC itself stores an address: a constant run is kept as it was
typed rather than split on every separator. There is no canonical
segmentation, though, so this expands an address rather than normalising
one -- feeding a loaded message's partials back through it will not
necessarily reproduce them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
address
|
str
|
The address, with |
required |
Returns:
| Type | Description |
|---|---|
list[Partial]
|
The partials, in order, ready for |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the address is empty, or a brace is unmatched or empty. |
Source code in src/py2tosc/ui.py
Bindings¶
osc ¶
osc(
address: str = "/{name}",
*,
args: Sequence[Partial] | None = None,
on: TriggerCondition | str = TriggerCondition.ANY,
var: str = "x",
triggers: Sequence[Trigger] | None = None,
enabled: bool = True,
send: bool = True,
receive: bool = True,
feedback: bool = False,
no_duplicates: bool = False,
connections: str = ALL_CONNECTIONS,
) -> OscMessage
An OSC binding, addressed by an f-string-like template.
The defaults match OscMessage's: osc() sends the
control's x value to /<control name> on any change, over every
connection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
address
|
str
|
The OSC address, in the syntax |
'/{name}'
|
args
|
Sequence[Partial] | None
|
The arguments to send. Defaults to the control's |
None
|
on
|
TriggerCondition | str
|
When to fire -- |
ANY
|
var
|
str
|
The value watched by |
'x'
|
triggers
|
Sequence[Trigger] | None
|
Trigger objects, overriding |
None
|
enabled
|
bool
|
Whether the binding is active. |
True
|
send
|
bool
|
Whether the control transmits. |
True
|
receive
|
bool
|
Whether the control accepts incoming messages. |
True
|
feedback
|
bool
|
Whether received messages are echoed back. |
False
|
no_duplicates
|
bool
|
Whether to suppress repeated identical messages. |
False
|
connections
|
str
|
One character per connection slot, |
ALL_CONNECTIONS
|
Returns:
| Type | Description |
|---|---|
OscMessage
|
The binding. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the address cannot be expanded. |
Source code in src/py2tosc/ui.py
midi_cc ¶
midi_cc(
controller: int | Partial,
*,
channel: int | Partial = 0,
scale: tuple[float, float] = (0.0, 127.0),
source: Partial | str = "x",
on: TriggerCondition | str = TriggerCondition.ANY,
var: str = "x",
triggers: Sequence[Trigger] | None = None,
enabled: bool = True,
send: bool = True,
receive: bool = True,
feedback: bool = False,
no_duplicates: bool = False,
connections: str = ALL_CONNECTIONS,
) -> MidiMessage
A MIDI control change binding.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
controller
|
int | Partial
|
The CC number, 0-127, or a partial to draw it from. |
required |
channel
|
int | Partial
|
The MIDI channel, 0-15, or a partial to draw it from. |
0
|
scale
|
tuple[float, float]
|
Low and high ends of the range the value is sent over. Applies
only when |
(0.0, 127.0)
|
source
|
Partial | str
|
The control value driving the message, or a partial. |
'x'
|
on
|
TriggerCondition | str
|
When to fire -- |
ANY
|
var
|
str
|
The value watched by |
'x'
|
triggers
|
Sequence[Trigger] | None
|
Trigger objects, overriding |
None
|
enabled
|
bool
|
Whether the binding is active. |
True
|
send
|
bool
|
Whether the control transmits. |
True
|
receive
|
bool
|
Whether the control accepts incoming messages. |
True
|
feedback
|
bool
|
Whether received messages are echoed back. |
False
|
no_duplicates
|
bool
|
Whether to suppress repeated identical messages. |
False
|
connections
|
str
|
One character per connection slot, |
ALL_CONNECTIONS
|
Returns:
| Type | Description |
|---|---|
MidiMessage
|
The binding. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py2tosc/ui.py
midi_note ¶
midi_note(
note: int | Partial,
*,
channel: int | Partial = 0,
scale: tuple[float, float] = (0.0, 127.0),
source: Partial | str = "x",
on: TriggerCondition | str = TriggerCondition.ANY,
var: str = "x",
triggers: Sequence[Trigger] | None = None,
enabled: bool = True,
send: bool = True,
receive: bool = True,
feedback: bool = False,
no_duplicates: bool = False,
connections: str = ALL_CONNECTIONS,
) -> MidiMessage
A MIDI note-on binding, with the control's value as velocity.
A whole keyboard of buttons can name its own notes rather than being numbered one at a time, which is what the corpus does:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
note
|
int | Partial
|
The note number, 0-127, or a partial to draw it from. |
required |
channel
|
int | Partial
|
The MIDI channel, 0-15, or a partial to draw it from. |
0
|
scale
|
tuple[float, float]
|
Low and high ends of the velocity range. Applies only when
|
(0.0, 127.0)
|
source
|
Partial | str
|
The control value driving the velocity, or a partial. |
'x'
|
on
|
TriggerCondition | str
|
When to fire -- |
ANY
|
var
|
str
|
The value watched by |
'x'
|
triggers
|
Sequence[Trigger] | None
|
Trigger objects, overriding |
None
|
enabled
|
bool
|
Whether the binding is active. |
True
|
send
|
bool
|
Whether the control transmits. |
True
|
receive
|
bool
|
Whether the control accepts incoming messages. |
True
|
feedback
|
bool
|
Whether received messages are echoed back. |
False
|
no_duplicates
|
bool
|
Whether to suppress repeated identical messages. |
False
|
connections
|
str
|
One character per connection slot, |
ALL_CONNECTIONS
|
Returns:
| Type | Description |
|---|---|
MidiMessage
|
The binding. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py2tosc/ui.py
connect ¶
connect(
dst: Control | str,
*,
source: Partial | str = "x",
to: Partial | str = "x",
on: TriggerCondition | str = TriggerCondition.ANY,
var: str = "x",
triggers: Sequence[Trigger] | None = None,
enabled: bool = True,
) -> LocalMessage
A binding to another control in the same layout.
Both ends are described with the same three constructors used for OSC
arguments, so source=prop("name"), to="text" reads as one idea rather
than seven keyword arguments:
connect(readout, source=prop("name"), to="text", on="RISE")
connect(readout, source=const("0"), to=prop("sum"), on="RISE")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dst
|
Control | str
|
The destination control, or its id. Passing a control reads its
|
required |
source
|
Partial | str
|
What to send. A bare string names one of this control's values. |
'x'
|
to
|
Partial | str
|
What to write on the destination. A bare string names one of its
values; a |
'x'
|
on
|
TriggerCondition | str
|
When to fire -- |
ANY
|
var
|
str
|
The value watched by |
'x'
|
triggers
|
Sequence[Trigger] | None
|
Trigger objects, overriding |
None
|
enabled
|
bool
|
Whether the binding is active. |
True
|
Returns:
| Type | Description |
|---|---|
LocalMessage
|
The binding. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py2tosc/ui.py
Layout¶
These describe an arrangement instead of applying one. Each returns the container it built, so the result goes wherever a control goes and layouts nest by ordinary composition. Frames are assigned later, by resolve.
The first four build a GROUP, arranging controls you already have.
row ¶
row(
*children: Control,
sizes: int | Sequence[float] | None = None,
gap: float | Sequence[float] = 0,
pad: float | Sequence[float] = 0,
**props: Any,
) -> Control
Arrange controls left to right inside a group.
The group is returned rather than the children, so the result goes wherever a control goes and layouts nest by ordinary composition:
No frames are assigned here. The arrangement is recorded and applied by
resolve once the frame at the top is known, which is
what lets a layout be described from the inside out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*children
|
Control
|
The controls to arrange, in order. |
()
|
sizes
|
int | Sequence[float] | None
|
Relative widths, one per child. Omitted, they share equally. |
None
|
gap
|
float | Sequence[float]
|
Space between children, in pixels. |
0
|
pad
|
float | Sequence[float]
|
Inset around the whole row -- a number, a |
0
|
**props
|
Any
|
Properties to set on the group, such as |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
column ¶
column(
*children: Control,
sizes: int | Sequence[float] | None = None,
gap: float | Sequence[float] = 0,
pad: float | Sequence[float] = 0,
**props: Any,
) -> Control
Arrange controls top to bottom inside a group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*children
|
Control
|
The controls to arrange, in order. |
()
|
sizes
|
int | Sequence[float] | None
|
Relative heights, one per child. Omitted, they share equally. |
None
|
gap
|
float | Sequence[float]
|
Space between children, in pixels. |
0
|
pad
|
float | Sequence[float]
|
Inset around the whole column. |
0
|
**props
|
Any
|
Properties to set on the group. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
tiles ¶
tiles(
*children: Control,
columns: int = 4,
rows: int | None = None,
gap: float | Sequence[float] = 0,
pad: float | Sequence[float] = 0,
**props: Any,
) -> Control
Tile controls in a grid, filling row by row.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*children
|
Control
|
The controls to arrange, in row-major order. |
()
|
columns
|
int
|
How many columns. |
4
|
rows
|
int | None
|
How many rows, or |
None
|
gap
|
float | Sequence[float]
|
Space between cells, in pixels. |
0
|
pad
|
float | Sequence[float]
|
Inset around the whole grid. |
0
|
**props
|
Any
|
Properties to set on the group. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
stack ¶
Overlay controls, each filling the group.
A label sitting on a button is the commonest idiom in TouchOSC and the one the eager layout functions cannot express at all, since they divide a frame rather than share it. The last child is on top.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*children
|
Control
|
The controls to overlay, back to front. |
()
|
pad
|
float | Sequence[float]
|
Inset applied to every child. |
0
|
**props
|
Any
|
Properties to set on the group. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
The last two build the control the format names, which cannot be a plain group: a PAGER pages between its children, and a GRID holds copies of one control type.
pager ¶
Stack groups as the pages of a PAGER.
A pager shows one page at a time and switches between them itself, so every
page gets the same frame -- the arrangement stack
makes, on a PAGER rather than a GROUP, and minus the tab bar. A page
sized to the whole pager would sit underneath the tabs, so resolve reads
the pager's own tabbar, tabbar_size and orientation and reserves that
much from whichever edge the bar is on. With tabbar off a page fills the
pager exactly, which is what most of the corpus does.
Pages should be groups, which the other combinators already return:
The tab a page is reached by shows its tab_label, which is a different
property from its name. A page with a name and no label of its own is
given its name, since a pager whose tabs are all blank is not usable; set
tab_label on the page to say something else.
A page also styles its own tab, through tab_color_on, tab_color_off,
text_color_on and text_color_off. Those belong to the page rather than
to the pager, so no control type declares them as defaults, and a page left
without them draws its label in no colour at all -- a tab bar with nothing
written on it. Any it does not already carry are filled in with the values
the corpus agrees on. The editor also makes its pages non-interactive and
un-outlined; a group defaults both the other way, and since an untouched
default cannot be told from a deliberate one, that is left to you.
A pager must not be the document root. TouchOSC treats the root as the
canvas and gives it none of its type's behaviour, so a PAGER there draws
a tab bar and then stacks every page instead of paging between them. Put it
inside a group. Both that and a page that is not a GROUP are reported by
validate rather than rejected here, since TouchOSC
loads them either way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*pages
|
Control
|
The pages, in tab order. |
()
|
pad
|
float | Sequence[float]
|
Inset applied to every page, on top of the tab bar. |
0
|
**props
|
Any
|
Properties to set on the pager. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
grid ¶
grid(
control_type: ControlType | str,
*,
columns: int = 2,
rows: int = 2,
**props: Any,
) -> Control
Build a GRID control: one control replicated across its cells.
This is the format's own GRID, the same control
py2tosc.grid makes, but with the cells it must hold --
TouchOSC has no empty grid. It fills itself with columns * rows controls
of one type, which is what a multitoggle or a bank of faders is; every grid
in the corpus holds a single type, so the type is what it takes rather than
a list of children.
To arrange controls you already have, and get a GROUP rather than a
GRID, use tiles.
A GRID tiles its cells itself rather than dividing its frame the way a
layout does: every cell is the same size, with a three-point margin around
and between, and whatever will not divide evenly is left at the far edge.
Frames are assigned by resolve, as everywhere else.
Cells are named 1 upwards, in the order grid_order and grid_start
describe -- by default across each row from the top left. Reach them
through the returned control to give them messages:
pads = matrix("BUTTON", columns=8, rows=8, name="multitoggle")
for cell in pads:
cell.messages.append(midi_note(prop("name")))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control_type
|
ControlType | str
|
The control to replicate. |
required |
columns
|
int
|
Cells across, written as |
2
|
rows
|
int
|
Cells down, written as |
2
|
**props
|
Any
|
Properties to set on the grid itself. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py2tosc/ui.py
Resolving¶
resolve ¶
Assign frames to everything the layout combinators described.
Placement runs top down, because a layout can only divide a frame it knows.
A parent decides its children's frames outright: a control inside a layout
does not keep a frame it was built with. To place something by hand, leave
it out of a layout group, or put it in a stack.
Document.resolve calls this against the root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control
|
Control
|
The control to place, along with everything beneath it. |
required |
frame
|
Sequence[float] | None
|
The frame to give |
None
|
Returns:
| Type | Description |
|---|---|
Control
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a layout cannot fit its children into the space it has,
or if |
Source code in src/py2tosc/ui.py
Idioms¶
Thin wrappers over the two layers above.
labelled ¶
labelled(
control: Control,
text: str,
*,
size: float = 48,
inset: float | Sequence[float] = 0.0,
**props: Any,
) -> Control
A control with a caption laid over it.
The label is not interactive and has no background, so the control beneath receives the touch and shows through. It takes the control's colour and its name is the caption, which is what a local message sends when the control is pressed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control
|
Control
|
The control to caption. |
required |
text
|
str
|
The caption, which is also the label's name. |
required |
size
|
float
|
Text size. |
48
|
inset
|
float | Sequence[float]
|
A fraction of the frame to inset the caption by, for padding. The control underneath is not inset. |
0.0
|
**props
|
Any
|
Properties to set on the group holding the two. |
{}
|
Returns:
| Type | Description |
|---|---|
Control
|
A |
Source code in src/py2tosc/ui.py
inset ¶
Shrink a control within the frame its layout gives it.
Padding is in pixels, which is no use for anything proportional in a deferred layout: the size to take a fraction of is not known until the frame comes down from above. An inset is a fraction, applied then.
Unlike a group's pad, this belongs to one control, so a stack can inset
its label without insetting the button underneath -- which is what the
caption on a key needs, and the one thing pad cannot say.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control
|
Control
|
The control to inset. It is modified and returned, so this reads as a wrapper without building a group to be one. |
required |
amount
|
float | Sequence[float]
|
A fraction of the frame -- a number, a
|
required |
Returns:
| Type | Description |
|---|---|
Control
|
|