manatools.aui is a backend-agnostic UI abstraction layer inspired by libyui. It supports three backends: GTK4, Qt6 (PySide6) and NCurses. Application code uses a single API regardless of the backend in use.
from manatools.aui.yui import YUI, YUI_uiThe backend is selected at startup by reading the MUI_BACKEND environment variable. Accepted values are gtk, qt and ncurses (case-insensitive). When the variable is not set, backends are probed in this order: Qt → GTK → NCurses.
import os
os.environ["MUI_BACKEND"] = "gtk" # force GTK backend
from manatools.aui.yui import YUI, YUI_uiui = YUI.ui() # YUI singleton (also: YUI_ui())
app = YUI.app() # backend application object
# aliases: YUI.application(), YUI.yApp()
factory = ui.widgetFactory()For the NCurses backend, always call ui.shutdown() before the process exits to restore the terminal:
ui = YUI_ui()
try:
...
finally:
ui.shutdown() # NCurses only; no-op on GUI backendsAll enums are importable from manatools.aui.yui (they are re-exported from yui_common).
class YUIDimension(Enum):
YD_HORIZ = 0 # Horizontal = 0 (alias)
Horizontal = 0
YD_VERT = 1 # Vertical = 1 (alias)
Vertical = 1class YAlignmentType(Enum):
YAlignUnchanged = 0 # do not force alignment
YAlignBegin = 1 # left / top
YAlignEnd = 2 # right / bottom
YAlignCenter = 3 # centerclass YDialogType(Enum):
YMainDialog = 0
YPopupDialog = 1
YWizardDialog = 2class YDialogColorMode(Enum):
YDialogNormalColor = 0
YDialogInfoColor = 1
YDialogWarnColor = 2class YEventType(Enum):
NoEvent = 0
WidgetEvent = 1
MenuEvent = 2
KeyEvent = 3
CancelEvent = 4
TimeoutEvent = 5class YEventReason(Enum):
Activated = 0
ValueChanged = 1
SelectionChanged = 2class YCheckBoxState(Enum):
YCheckBox_dont_care = -1 # tri-state: indeterminate
YCheckBox_off = 0
YCheckBox_on = 1class YButtonRole(Enum):
YCustomButton = 0
YOKButton = 1
YCancelButton = 2
YHelpButton = 3Controls the automatic-scroll policy of a YLogView widget.
class YLogViewFocus(Enum):
HEAD = 0 # anchor at first line — no auto-scroll on append (default)
TAIL = 1 # follow last line — scroll to newest on every append| Value | Behaviour |
|---|---|
HEAD |
The viewport stays where it is when appendLines() is called. The user must scroll manually to see new content. |
TAIL |
After every appendLines() call the widget automatically scrolls to the end of the rendered text. In normal order (oldest-at-top) the end is the newest line; in reverse order (newest-at-top) the end is the oldest line. The same scroll is applied when the widget is first shown if lines were added before the dialog was displayed. |
class YUIException(Exception): ...
class YUIWidgetNotFoundException(YUIException): ...
class YUINoDialogException(YUIException): ...
class YUIInvalidWidgetException(YUIException): ...All exceptions derive from YUIException. YUINoDialogException is raised when dialog-level operations (e.g. currentDialog()) are called with no open dialog.
All event objects are returned by dialog.waitForEvent() or dialog.pollEvent().
ev.eventType() -> YEventType
ev.widget() -> YWidget | None # widget that generated the event
ev.reason() -> YEventReason | None
ev.serial() -> intGenerated when the user interacts with a widget (button press, field change, etc.).
isinstance(ev, YWidgetEvent) # True when ev.eventType() == YEventType.WidgetEvent
ev.widget() # the widget
ev.reason() # e.g. YEventReason.Activatedev.keySymbol() -> str # e.g. "F1", "Return", "Escape"
ev.focusWidget() -> YWidget | NoneGenerated when a menu item is activated.
ev.item() -> YMenuItem | None
ev.id() -> str | NoneFired when waitForEvent(timeout_millisec) times out before a user action.
ev.eventType() == YEventType.TimeoutEventGenerated when the user closes the dialog via the window manager (close button, Escape, Ctrl-C in NCurses).
ev.eventType() == YEventType.CancelEventEvery widget returned by the factory inherits from YWidget. The methods below are available on all widgets regardless of backend.
w.id() -> str # auto-generated unique identifier
w.widgetClass() -> str # class name string (e.g. "YPushButton")
w.widgetPathName() -> str # full path in widget hierarchy
w.debugLabel() -> str # human-readable label for debuggingw.parent() -> YWidget | None
w.hasParent() -> bool
w.firstChild() -> YWidget | None
w.lastChild() -> YWidget | None
w.childrenCount() -> int
w.hasChildren() -> bool
w.addChild(child)
w.removeChild(child)
w.deleteChildren() # remove all logical children
w.findDialog() -> YWidget | None # nearest ancestor dialogw.setEnabled(enabled: bool = True)
w.setDisabled() # shortcut for setEnabled(False)
w.isEnabled() -> boolw.setVisible(visible: bool = True)
w.visible() -> boolWeights control how extra space is distributed among siblings in a container. The values are non-negative integers; what matters is the ratio between sibling weights.
w.setWeight(dim: YUIDimension, weight: int)
w.weight(dim: YUIDimension) -> intExample — split a VBox in a 2/3 : 1/3 ratio:
top_widget.setWeight(YUIDimension.YD_VERT, 67)
bot_widget.setWeight(YUIDimension.YD_VERT, 33)Weights of 0 (the default) mean the widget has no preference; allocation falls back to equal distribution or natural sizes.
w.setStretchable(dim: YUIDimension, stretch: bool)
w.stretchable(dim: YUIDimension) -> boolA stretchable widget expands to fill available space in the given dimension. setWeight implies stretchability on that axis.
w.setNotify(notify: bool = True)
w.notify() -> boolWhen notify is True (default), the widget posts a YWidgetEvent each time its value or selection changes.
w.setHelpText(text: str)
w.helpText() -> strw.setFunctionKey(fkey_no: int) # e.g. 1 → F1
w.functionKey() -> intDialogs are created via the factory (see §7) and manage the event loop.
dialog.currentDialog(doThrow=True) -> YDialog # topmost open dialog
dialog.topmostDialog(doThrow=True) -> YDialog # alias
dialog.isTopmostDialog() -> booldialog.open() # present the dialog (called automatically by waitForEvent)
dialog.isOpen() -> bool
dialog.destroy(doThrow=True) # close and remove from stackev = dialog.waitForEvent(timeout_millisec: int = 0) -> YEventBlocks until an event is available. If timeout_millisec > 0 a YTimeoutEvent is delivered after the interval. In GTK and Qt the call runs a nested event-loop iteration so other windows remain responsive.
ev = dialog.pollEvent() -> YEvent | NoneNon-blocking variant; returns None immediately if no event is pending (not all backends guarantee full support).
while True:
ev = dlg.waitForEvent()
if ev.eventType() == YEventType.CancelEvent:
break
if ev.eventType() == YEventType.WidgetEvent:
if ev.widget() == ok_btn:
# handle OK
break
dlg.destroy()The object returned by YUI.app() is the backend-specific Application object. Obtain it with:
app = YUI.app() # YUI.application() and YUI.yApp() are aliasesapp.setApplicationTitle(title: str)
app.applicationTitle() -> str
app.setApplicationIcon(icon_spec: str) # theme name or file path
app.applicationIcon() -> str
app.setIconBasePath(path: str) # prefix for icon resolution
app.iconBasePath() -> strapp.setProductName(name: str) / app.productName() -> str
app.setApplicationName(name: str) / app.applicationName() -> str
app.setVersion(version: str) / app.version() -> str
app.setAuthors(authors: str) / app.authors() -> str
app.setDescription(desc: str) / app.description() -> str
app.setLicense(text: str) / app.license() -> str
app.setCredits(credits: str) / app.credits() -> str
app.setInformation(info: str) / app.information() -> str
app.setLogo(path: str) / app.logo() -> strapp.isTextMode() -> bool # True for NCurses backend
app.busyCursor() # show busy indicator (best-effort)
app.normalCursor() # restore normal cursorBlocking helpers that return the selected path or "" when canceled.
app.askForExistingDirectory(startDir: str, headline: str) -> str
app.askForExistingFile(startWith: str, filter: str, headline: str) -> str
app.askForSaveFileName(startWith: str, filter: str, headline: str) -> strfilter is a semicolon-separated list of glob patterns (e.g. "*.txt;*.md"). GTK uses Gtk.FileDialog (GTK 4.10+, portal-aware); Qt uses QFileDialog (synchronous); NCurses renders an in-terminal browser overlay.
app = YUI.app()
app.setApplicationTitle("MyApp")
app.setIconBasePath("/usr/share/myapp/icons")
app.setApplicationIcon("myapp-icon") # theme name or absolute path
fn = app.askForExistingFile("/home/user", "*.iso;*.img", "Open image")
if fn:
print("Selected:", fn)The factory is obtained from the UI singleton:
factory = YUI.ui().widgetFactory()
# or equivalently:
factory = YUI_ui().widgetFactory()All createXxx() calls return a YWidget subclass with the backend widget attached. The parent argument must be an open dialog or a container widget that has already been added to a dialog.
factory.createMainDialog(color_mode=YDialogColorMode.YDialogNormalColor) -> YDialog
factory.createPopupDialog(color_mode=YDialogColorMode.YDialogNormalColor) -> YDialogA dialog is the root container. Every widget tree must start from a dialog. The color_mode hint changes the visual style on capable backends (GTK / Qt apply a tint; NCurses may adjust attribute sets).
factory.createVBox(parent) -> YWidget # stack children vertically
factory.createHBox(parent) -> YWidget # stack children horizontallyChildren add themselves to the container in creation order. To control how the space is divided, call setWeight() on each child after creation (see §5).
factory.createPaned(parent, dimension: YUIDimension = YUIDimension.YD_HORIZ) -> YWidgetA two-child split container with a resizable divider. dimension controls the split axis:
YUIDimension.YD_HORIZ— side-by-side panesYUIDimension.YD_VERT— top/bottom panes
The initial split position is derived from the weight() values of the two children (first-child weight / total weight). Default is 50/50.
paned = factory.createPaned(dlg, YUIDimension.YD_VERT)
left = factory.createVBox(paned)
right = factory.createVBox(paned)
left.setWeight(YUIDimension.YD_VERT, 30)
right.setWeight(YUIDimension.YD_VERT, 70)factory.createLeft(parent) # align child to left (YAlignBegin / YAlignUnchanged)
factory.createRight(parent) # align child to right (YAlignEnd / YAlignUnchanged)
factory.createTop(parent) # align child to top (YAlignUnchanged / YAlignBegin)
factory.createBottom(parent) # align child to bottom (YAlignUnchanged / YAlignEnd)
factory.createHCenter(parent) # center child horizontally
factory.createVCenter(parent) # center child vertically
factory.createHVCenter(parent) # center in both axesEach returns a single-child container; add exactly one child widget to it.
factory.createAlignment(parent,
horAlignment: YAlignmentType,
vertAlignment: YAlignmentType) -> YWidgetFor arbitrary combinations of YAlignmentType values.
factory.createMinWidth(parent, minWidth: int) -> YWidget # min width in pixels
factory.createMinHeight(parent, minHeight: int) -> YWidget # min height in pixels
factory.createMinSize(parent, minWidth: int, minHeight: int) -> YWidgetThese return single-child containers that impose a minimum size floor on their child. Internally they use an alignment widget with setMinWidth/setMinHeight applied.
factory.createSpacing(parent,
dim: YUIDimension,
stretchable: bool = False,
size_px: int = 0) -> YWidgetA blank spacer. When stretchable=True it expands to fill available space; when False it is a fixed gap of size_px pixels (pixel-to-character-cell conversion is applied on NCurses: 8 px/col horizontally, 16 px/row vertically).
Convenience variants:
factory.createHStretch(parent) # stretchable horizontal spacer
factory.createVStretch(parent) # stretchable vertical spacer
factory.createHSpacing(parent, size_px=8) # fixed horizontal gap (≈1 char)
factory.createVSpacing(parent, size_px=16) # fixed vertical gap (≈1 row)factory.createPushButton(parent, label: str) -> YWidget
factory.createIconButton(parent, iconName: str, fallbackTextLabel: str) -> YWidgetcreateIconButton uses the icon on GUI backends; falls back to fallbackTextLabel on NCurses. Both support setNotify(), setStretchable() and setFunctionKey() from the base API.
factory.createLabel(parent, text: str,
isHeading: bool = False,
isOutputField: bool = False) -> YWidget
factory.createHeading(parent, label: str) -> YWidget # shortcut: isHeading=TrueisOutputField=True styles the label as a read-only output field where supported.
factory.createInputField(parent, label: str, password_mode: bool = False) -> YWidget
factory.createPasswordField(parent, label: str) -> YWidget # alias: password_mode=True
factory.createMultiLineEdit(parent, label: str) -> YWidget
factory.createIntField(parent, label: str,
minVal: int, maxVal: int, initialVal: int) -> YWidgetcreatePasswordField is a convenience wrapper for createInputField(..., password_mode=True).
Common methods on input widgets:
w.value() -> str | int # current text / integer value
w.setValue(v) # set text / integer value
w.label() -> str
w.setLabel(label: str)factory.createCheckBox(parent, label: str, is_checked: bool = False) -> YWidgetValue access:
w.value() -> YCheckBoxState # YCheckBox_on / _off / _dont_care
w.setValue(state) # YCheckBoxState or boolfactory.createRadioButton(parent, label: str = "", isChecked: bool = False) -> YWidgetRadio buttons within the same container are automatically grouped. Use value() / setValue() for state access.
factory.createComboBox(parent, label: str, editable: bool = False) -> YWidgeteditable=False— drop-down list only.editable=True— allows free-text entry in addition to list selection.
Methods (inherited from YSelectionWidget):
w.addItem(item: YItem | str)
w.addItems(items: list[YItem | str])
w.deleteAllItems()
w.selectedItem() -> YItem | None
w.value() -> str # label of selected item
w.setValue(text: str) # select by labelfactory.createSelectionBox(parent, label: str) -> YWidget
factory.createMultiSelectionBox(parent, label: str) -> YWidgetSelectionBox allows one selection at a time; MultiSelectionBox allows zero or more. Both use the same YSelectionWidget API:
w.addItem(item: YItem | str)
w.addItems(items: list[YItem | str])
w.deleteAllItems()
w.selectItem(item: YItem, selected: bool = True)
w.selectedItem() -> YItem | None # first selected (single / first in multi)
w.selectedItems() -> list[YItem] # all selected (multi)
w.hasSelectedItem() -> bool
w.itemsCount() -> intfactory.createTree(parent, label: str,
multiselection: bool = False,
recursiveselection: bool = False) -> YWidgetItems are YTreeItem objects (see §9.2). Use the same YSelectionWidget methods (addItem, deleteAllItems, selectedItem, etc.) to populate and query.
factory.createTable(parent, header: YTableHeader, multiSelection: bool = False) -> YWidgetheader is a YTableHeader instance describing column titles and types (see §9.3). Rows are YTableItem objects (see §9.4).
Methods (from YSelectionWidget):
w.addItem(row: YTableItem)
w.addItems(rows: list[YTableItem])
w.deleteAllItems()
w.selectedItem() -> YTableItem | None
w.selectedItems() -> list[YTableItem]factory.createRichText(parent, text: str = "", plainTextMode: bool = False) -> YWidgetRenders HTML/rich text on GUI backends. plainTextMode=True disables markup interpretation.
w.setValue(text: str) # update content
w.value() -> strfactory.createLogView(
parent,
label: str,
visibleLines: int,
storedLines: int = 0,
focus: YLogViewFocus = YLogViewFocus.HEAD,
reverse: bool = False,
) -> YWidgetA scrollable, append-only text log area.
| Parameter | Description |
|---|---|
label |
Optional caption drawn above the text area. |
visibleLines |
Minimum number of text rows shown. |
storedLines |
Ring-buffer depth: the widget retains at most this many lines (0 = unlimited). |
focus |
Scroll-focus policy (see YLogViewFocus). HEAD (default) keeps the viewport pinned; TAIL auto-scrolls to the end of the rendered text on every appendLines() and also when the widget first becomes visible (if lines were pre-loaded before the dialog was shown). |
reverse |
Display order. False (default) = oldest line at the top, newest at the bottom. True = newest line at the top, oldest at the bottom. The internal buffer is always stored in chronological order; reverse only changes the rendering direction. |
lv.appendLines(text: str) # append one or more newline-separated lines
lv.clearText() # remove all lines
lv.logText() -> str # all retained lines joined with '\n'
lv.setLogText(text: str) # replace entire content
lv.lastLine() -> str # last line in retained buffer
lv.lines() -> int # number of retained lines
lv.label() -> str
lv.setLabel(label: str)
lv.visibleLines() -> int
lv.setVisibleLines(n: int)
lv.maxLines() -> int
lv.setMaxLines(n: int)
# Focus and display-order accessors (runtime changeable)
lv.focus() -> YLogViewFocus
lv.setFocus(f: YLogViewFocus) # change scroll policy without clearing
lv.reverse() -> bool
lv.setReverse(r: bool) # flip display order; internal buffer unchangedfrom manatools.aui.yui import YLogViewFocus
# --- TAIL mode: transaction log that always shows the latest message ---
# appendLines() scrolls to the bottom (newest line in normal order).
# Also works correctly if lines are pre-loaded before the dialog is shown.
tx_log = factory.createLogView(
vbox, "Transaction log", visibleLines=20, storedLines=2000,
focus=YLogViewFocus.TAIL)
tx_log.appendLines("Resolving dependencies...") # viewport scrolls to bottom
# --- TAIL + reverse mode: journal-style, newest at top, auto-scroll to bottom ---
# Display order: newest first (top). TAIL scrolls to the END of the rendered text,
# which in reverse order is the OLDEST line (bottom of the widget).
# This is the geometrical opposite of HEAD+normal: both show the oldest line
# in the viewport, but in reverse order the newest line is still visible at top.
journal = factory.createLogView(
vbox, "Recent events", visibleLines=10, storedLines=500,
focus=YLogViewFocus.TAIL, reverse=True)
journal.appendLines("Service started") # viewport scrolls to bottom (oldest line)
# --- HEAD mode (default): progress trace, user scrolls manually ---
trace = factory.createLogView(vbox, "Trace", 8)
trace.appendLines("Build started\nCompiling foo.c\nCompiling bar.c")
# viewport stays at line 1 even after 100 more appends
# --- Runtime policy change ---
trace.setFocus(YLogViewFocus.TAIL) # start following new lines
trace.setReverse(True) # flip to newest-first; existing lines reversedfactory.createFrame(parent, label: str = "") -> YWidget
factory.createCheckBoxFrame(parent, label: str = "", checked: bool = False) -> YWidgetcreateFrame is a single-child decorative container with a border and optional title. createCheckBoxFrame adds a checkbox that enables/disables its child.
factory.createReplacePoint(parent) -> YWidgetA single-child placeholder whose content can be replaced at runtime by swapping the child widget.
factory.createDumbTab(parent) -> YWidgetA tabbed container without a built-in stack; the application is responsible for showing/hiding child panes when tab events arrive.
factory.createMenuBar(parent) -> YWidgetA menu bar. Populate it by adding YMenuItem objects (see §9.1). A YMenuEvent is delivered when the user selects an item.
factory.createProgressBar(parent, label: str, max_value: int = 100) -> YWidgetw.setValue(value: int) # current progress (0 … max_value)
w.value() -> intfactory.createSlider(parent, label: str,
minVal: int, maxVal: int, initialVal: int) -> YWidgetw.setValue(value: int)
w.value() -> intfactory.createDateField(parent, label: str) -> YWidget
factory.createTimeField(parent, label: str) -> YWidgetValue is a string in ISO format ("YYYY-MM-DD" / "HH:MM:SS").
w.value() -> str
w.setValue(text: str)factory.createImage(parent,
imageFileName: str,
fallBackName: str = None) -> YWidgetDisplays an image on GUI backends (GTK4, Qt6). On the NCurses backend, where pixel rendering is not possible, a decorative box frame is drawn instead with a short label inside.
| Parameter | Description |
|---|---|
imageFileName |
Absolute path to an image file or a freedesktop theme icon name (e.g. "dialog-warning", "application-exit"). |
fallBackName |
(optional) Text shown centred inside the NCurses placeholder frame. When omitted the basename of imageFileName is used. Ignored by GUI backends. |
Two independent mechanisms control how the image fills its allocated space:
setStretchable(dim, True) — marks the widget as willing to expand in the given dimension. The layout manager gives the widget all available space in that axis (equivalent to hexpand/vexpand in GTK4 and QSizePolicy.Expanding in Qt6). The image is then scaled to fill the allocated area, respecting each axis independently. Calling it on both axes produces a fully-stretched image that deforms freely.
setAutoScale(True) — equivalent to setting both axes stretchable but with a preserved aspect ratio. The image is scaled to fit within the allocated area (width × height) using the intrinsic aspect ratio. This is the recommended mode for banners or logos.
img = factory.createImage(vbox, "/usr/share/myapp/logo.png")
# Fill available width, deform height to match:
img.setStretchable(YUIDimension.YD_HORIZ, True)
img.setStretchable(YUIDimension.YD_VERT, True)
# OR: scale to fit preserving aspect ratio:
img.setAutoScale(True)
# Fixed size icon (e.g. dialog decoration, no stretching):
img.setStretchable(YUIDimension.YD_HORIZ, False)
img.setStretchable(YUIDimension.YD_VERT, False)
img.setAutoScale(False)img.setImage(newPath: str) # reload from a new file or theme name
img.imageFileName() -> str # current source path / icon name
img.autoScale() -> bool| Backend | Behaviour |
|---|---|
| GTK4 | Uses Gtk.Image backed by GdkPixbuf. Theme icons are resolved via Gio.ThemedIcon. Scaling is applied on every GTK size-allocate signal so the image always fills its allocation when stretchable. do_measure returns minimum = 0 on stretchable axes, ensuring GTK layouts can freely expand the widget. |
| Qt6 | Uses a QLabel subclass. Stretching is applied in resizeEvent so the image always tracks the label's allocated size. No hardcoded minimum size is enforced — the widget sizes to its natural pixmap dimensions when not stretchable, and expands freely when setStretchable or setAutoScale is used. |
| NCurses | No pixel rendering. A Unicode box is drawn at the widget's character-cell allocation. The fallBackName text (or image basename) is displayed centred inside the frame. |
# Dialog warning icon with a meaningful NCurses label
icon = factory.createImage(parent, "dialog-warning", fallBackName="!")
# Application logo with descriptive text
logo = factory.createImage(parent, "/usr/share/myapp/logo.png",
fallBackName="MyApp")Used by selection widgets (ComboBox, SelectionBox, MultiSelectionBox).
item = YItem(label: str, selected: bool = False, icon_name: str = "")
item.label() -> str
item.setLabel(label: str)
item.selected() -> bool
item.setSelected(selected: bool = True)
item.iconName() -> str
item.setIconName(name: str)
item.hasIconName() -> bool
item.index() -> int
item.data() -> any # arbitrary application data
item.setData(data)Used with createMenuBar(). Items can be nested arbitrarily.
menu = YMenuItem(label: str, icon_name: str = "",
enabled: bool = True,
is_menu: bool = False,
is_separator: bool = False)
menu.label() -> str
menu.setLabel(label: str)
menu.iconName() -> str
menu.setIconName(name: str)
menu.enabled() -> bool
menu.setEnabled(on: bool = True)
menu.visible() -> bool
menu.setVisible(on: bool = True) # propagates to children
menu.isMenu() -> bool
menu.isSeparator() -> bool
menu.parentItem() -> YMenuItem | None
menu.hasChildren() -> bool
# Building the tree:
child_item = menu.addItem(label: str, icon_name: str = "") -> YMenuItem
submenu = menu.addMenu(label: str, icon_name: str = "") -> YMenuItem
sep = menu.addSeparator() -> YMenuItemTree items for createTree(). YTreeItem extends YItem and can have child items.
node = YTreeItem(label: str,
parent: YTreeItem = None,
selected: bool = False,
is_open: bool = False,
icon_name: str = "")
# Inherited from YItem:
node.label() / node.setLabel()
node.selected() / node.setSelected()
node.iconName() / node.setIconName() / node.hasIconName()
node.data() / node.setData()
# Tree-specific:
node.isOpen() -> bool
node.parentItem() -> YTreeItem | None
node.hasChildren() -> bool
child = node.addChild(item: YTreeItem | str) -> YTreeItemNesting example:
root = YTreeItem("Root", is_open=True)
child = root.addChild("Child")
grandchild = YTreeItem("Grandchild", parent=child)
tree_widget.addItem(root)Describes the column structure of a createTable() widget.
header = YTableHeader()
header.addColumn(header: str,
checkBox: bool = False,
alignment: YAlignmentType = YAlignmentType.YAlignBegin)
header.columns() -> int
header.hasColumn(col: int) -> bool
header.header(col: int) -> str
header.isCheckboxColumn(col: int) -> bool
header.alignment(col: int) -> YAlignmentTypeA single cell within a table row.
cell = YTableCell(label: str = "",
icon_name: str = "",
sort_key: str = "",
parent: YTableItem = None,
column: int = -1,
checked: bool = None) # None = not a checkbox column
cell.label() -> str
cell.setLabel(label: str)
cell.iconName() -> str
cell.setIconName(name: str)
cell.hasIconName() -> bool
cell.sortKey() -> str
cell.hasSortKey() -> bool
cell.column() -> int
cell.parent() -> YTableItem | None
cell.itemIndex() -> int # row index in the table
cell.checked() -> bool # False when not a checkbox cell
cell.setChecked(val: bool = True)A table row. Extends YTreeItem and holds a list of YTableCell.
row = YTableItem(label: str = "")
# Convenience constructors:
row.addCell(cell_or_label, icon_name="", sort_key="")
row.addCells(*labels) # e.g. row.addCells("Alice", "30", "Engineer")
row.deleteCells()
# Cell access:
row.cellCount() -> int
row.hasCell(index: int) -> bool
row.cell(index: int) -> YTableCell | None
# Convenience accessors (column 0 by default):
row.label(index: int = 0) -> str
row.iconName(index: int = 0) -> str
row.checked(index: int = 0) -> boolBoth VBox and HBox distribute extra space among their children proportionally to their weights on the relevant axis. Children with weight 0 receive space only as needed; children with non-zero weights share the remaining space.
# 2/3 – 1/3 vertical split inside a VBox
top.setWeight(YUIDimension.YD_VERT, 2)
bot.setWeight(YUIDimension.YD_VERT, 1)Absolute values are not significant — only ratios. setWeight(dim, 67) and setWeight(dim, 33) produce the same result as setWeight(dim, 2) and setWeight(dim, 1).
createPaned derives the initial split position from the first child's weight divided by the total weight of both children. If both children have weight 0 the divider starts at 50%. Weights also set the QSplitter stretch factors on Qt and the initial Gtk.Paned position on GTK.
setStretchable(dim, True) marks a widget as willing to expand; setWeight additionally controls the proportion. Use weights when siblings should share an unequal ratio; use stretchable for widgets that should simply fill remaining space equally.
# three buttons; middle one gets twice as much horizontal space:
left.setStretchable(YUIDimension.YD_HORIZ, True)
mid.setWeight(YUIDimension.YD_HORIZ, 2)
right.setStretchable(YUIDimension.YD_HORIZ, True)import os
from manatools.aui.yui import (
YUI, YUI_ui,
YItem, YTreeItem, YTableHeader, YTableItem,
YEventType, YEventReason, YUIDimension, YDialogColorMode,
)
ui = YUI_ui()
factory = ui.widgetFactory()
# Main dialog
dlg = factory.createMainDialog()
vbox = factory.createVBox(dlg)
# Combo
combo = factory.createComboBox(vbox, "Choose language:")
combo.addItems([YItem("Python", selected=True), YItem("Ruby"), YItem("Rust")])
# Splitter: tree on left, log on right
paned = factory.createPaned(vbox, YUIDimension.YD_HORIZ)
left = factory.createVBox(paned)
right = factory.createVBox(paned)
left.setWeight(YUIDimension.YD_HORIZ, 40)
right.setWeight(YUIDimension.YD_HORIZ, 60)
tree = factory.createTree(left, "Packages:")
root = YTreeItem("base-system", is_open=True)
root.addChild("glibc")
root.addChild("bash")
tree.addItem(root)
log = factory.createLogView(right, "Output:", visibleLines=8)
# Buttons row
hbox = factory.createHBox(vbox)
factory.createHStretch(hbox) # push buttons to the right
ok_btn = factory.createPushButton(hbox, "OK")
cancel_btn = factory.createPushButton(hbox, "Cancel")
# Event loop
while True:
ev = dlg.waitForEvent()
if ev.eventType() == YEventType.CancelEvent:
break
if ev.eventType() == YEventType.WidgetEvent:
if ev.widget() == cancel_btn:
break
if ev.widget() == ok_btn:
sel = tree.selectedItem()
print("Package:", sel.label() if sel else "(none)")
print("Language:", combo.value())
break
dlg.destroy()
# NCurses backend: restore terminal
if app.isTextMode():
ui.shutdown()| Feature | GTK | Qt | NCurses |
|---|---|---|---|
| Icons in widgets | Yes (GdkPixbuf) | Yes (QIcon) | Ignored |
| File dialogs | Gtk.FileDialog (GTK 4.10+) | QFileDialog | In-terminal overlay |
busyCursor() |
Best-effort | QApplication override cursor | No-op |
createPaned divider |
Gtk.Paned (idle-deferred) | QSplitter | Simulated in chars |
createImage |
Full image rendering (GdkPixbuf; scales on size-allocate when stretchable) | Full image rendering (QLabel+QPixmap; scales on resizeEvent when stretchable) | Unicode box frame with fallBackName text |
createRichText |
HTML via WebKit / Gtk.Label | QTextBrowser | Plain text fallback |
| Scrollbar auto-scroll | Supported | Supported | Limited |
shutdown() required |
No | No | Yes |
All createXxx() methods are implemented across all three backends unless noted above.
manatools.ui provides ready-made helpers and base classes that sit on top of the AUI factory API. They handle the dialog lifecycle, event loop and title-bar save/restore automatically, so callers only need to supply a small info dictionary.
from manatools.ui import common
from manatools.ui.basedialog import BaseDialog, DialogType
from manatools.ui.helpdialog import HelpDialogAll helpers accept an info dictionary with the following keys. Keys marked optional may be omitted.
| Key | Type | Description |
|---|---|---|
title |
str |
Window/title-bar label (optional) |
text |
str |
Message body |
richtext |
bool |
Render text as HTML/rich markup (default False) |
default_button |
int |
Preselect button: 1 = affirmative, other = negative (optional) |
size |
dict | list/tuple |
Minimum size hint – see below |
# Dict form (pixels):
{'width': 480, 'height': 160}
# Legacy aliases accepted: 'column'/'columns', 'lines'/'rows'
# List / tuple form:
[480, 160] # [width, height]common.destroyUI()No-op stub kept for API compatibility with legacy code. AUI handles backend lifecycle internally.
Plain message dialog with a single Ok button.
common.msgBox({'title': 'Done', 'text': 'Operation completed.'})Returns 1 always; 0 if info is falsy.
Same as msgBox but displays a standard information icon next to the text.
Same as msgBox but displays a standard warning icon.
Dialog with Ok and Cancel buttons.
confirmed = common.askOkCancel({
'title': 'Confirm',
'text': 'Delete selected items?',
'default_button': 1, # Ok is the default
})Returns True (Ok pressed) or False (Cancel / window close).
Same as askOkCancel but labels the buttons Yes and No.
Returns True (Yes) or False (No / window close).
Displays application metadata in classic or tabbed layout.
from manatools.ui.common import AboutDialog, AboutDialogMode
AboutDialog(
dialog_mode=AboutDialogMode.TABBED,
size={'width': 480, 'height': 320},
)Metadata (name, version, authors, description, license, credits, information, logo) is read from YUI.app() application attributes set earlier. The deprecated info dict overrides individual fields but emits a DeprecationWarning.
AboutDialogMode:
class AboutDialogMode(Enum):
CLASSIC = 1 # inline rich-text sections with Info / Credits buttons
TABBED = 2 # sections presented as DumbTab pagesBaseDialog is the base class for full application dialogs with an event manager.
class BaseDialog:
def __init__(self, title: str,
icon: str = "",
dialogType: DialogType = DialogType.MAIN,
minWidth: int = -1,
minHeight: int = -1): ...Subclasses must override:
def UIlayout(self, layout):
"""Build the dialog widget tree. `layout` is a VBox inside the dialog."""
raise NotImplementedErrorOptional override:
def doSomethingIntoLoop(self):
"""Called once per event-loop iteration after events are dispatched."""
passdialog.running -> bool # True while the event loop is active
dialog.timeout -> int # waitForEvent timeout in ms (0 = infinite)
dialog.factory -> YWidgetFactory
dialog.eventManager -> EventManagerd = MyDialog()
d.run() # blocks until ExitLoop() is called or dialog is closed
# inside any event handler:
self.ExitLoop() # requests the loop to stop after the current iterationDispatches YUI events to registered Python callbacks. Obtained via dialog.eventManager.
em = dialog.eventManager
# Widget events
em.addWidgetEvent(widget, callback)
em.addWidgetEvent(widget, callback, sendWidget=True) # callback(widget, event)
em.removeWidgetEvent(widget, callback)
# Menu events
em.addMenuEvent(menuItem, callback) # menuItem=None catches all
em.addMenuEvent(menuItem, callback, sendMenuItem=True)
em.removeMenuEvent(menuItem, callback)
# Timeout events
em.addTimeOutEvent(callback)
em.removeTimeOutEvent(callback)
# Cancel events
em.addCancelEvent(callback)
em.removeCancelEvent(callback)Callback signatures:
# Default (sendWidget/sendMenuItem = False):
def on_button(): ...
# With widget/item forwarding:
def on_button(widget, event): ...from manatools.ui import basedialog
class MyDialog(basedialog.BaseDialog):
def __init__(self):
super().__init__("My dialog", minWidth=400, minHeight=200)
def UIlayout(self, layout):
vbox = self.factory.createVBox(layout)
self.label = self.factory.createLabel(vbox, "Hello, world!")
btn = self.factory.createPushButton(vbox, "&Close")
self.eventManager.addWidgetEvent(btn, self.on_close)
def on_close(self):
self.ExitLoop()
MyDialog().run()A popup rich-text help browser based on BaseDialog.
from manatools.ui.helpdialog import HelpDialog
from manatools.basehelpinfo import HelpInfoBase
class MyHelp(HelpInfoBase):
def home(self):
return "<h2>Help</h2><p>Welcome to MyApp help.</p>"
def show(self, url):
# return HTML for internal links; None to open externally
return None
HelpDialog(MyHelp(), title="Help", minWidth=500, minHeight=350).run()- Internal links in rich text trigger
HelpInfoBase.show(url); if it returns a non-empty string the view is updated in place. - External URLs (where
show()returnsNone/empty) are opened viawebbrowser.open(). _normalize_dimension(value)is a static helper that returns a positive int or 0 for invalid values.
old_title/backupIconinitialization — all helpers initialize these variables toNonebefore thetryblock so thefinallyclause never raisesUnboundLocalErrorif dialog creation fails early.- Single
destroy()path — dialogs are destroyed exclusively in thefinallyblock; do not calldlg.destroy()elsewhere in the same function to avoid double-free. DialogType.MAINvsDialogType.POPUP—MAINcreates aYMainDialog;POPUPcreates aYPopupDialogand stacks on top of the current topmost dialog. Most helpers usePOPUP.
This document is part of the python-manatools project (LGPLv2+). Update both code docstrings and this file when the API changes.